◈ TRIADIC.LIFE / Engenharia de IA / Módulo 2 0/11 aulas
Módulo 02 · 32h estimadas

APIs de LLM e Engenharia de Contexto

No Módulo 1 você fez uma chamada. Aqui você aprende tudo o que cabe em uma requisição e como transformar um LLM em um componente confiável de software: com dados tipados, ferramentas, respostas em tempo real, custo sob controle e tratamento de falhas.

9 aulas 10 laboratórios Python + TypeScript Pré-requisito: Módulo 1

Ao final do módulo você vai

  • Escolher provedor e modelo com critérios de custo, qualidade, latência e privacidade.
  • Montar requisições completas: sistema, histórico, esforço, limites e motivos de parada.
  • Escrever prompts e montar contextos que funcionam com modelos de raciocínio atuais.
  • Receber dados tipados e validados, em vez de texto solto.
  • Dar ferramentas ao modelo e escrever o loop de tool calling à mão.
  • Fazer streaming, usar cache de prompt e batch, e calcular o efeito no custo.
  • Tratar erros, limites de taxa e timeouts como em qualquer sistema distribuído.

Preparando o ambiente

cd o-arco/curso-ia/codigo/modulo-02

pip install -r python/requirements.txt          # anthropic + pydantic
cd typescript && npm install && cd ..           # SDK + zod
cd projeto && npm install && cd ..              # projeto do módulo

export ANTHROPIC_API_KEY="sk-ant-..."
Orçamento dos labs Os labs deste módulo usam esforço low e respostas curtas. Rodar todos uma vez custa alguns centavos de dólar. Defina um limite de gasto no console do provedor mesmo assim: um loop com bug é a forma mais rápida de gastar dinheiro com IA.

2.1 Provedores e como escolher um modelo

Leitura · 40 min

Há três formas de acessar um LLM em produção:

FormaExemplosVantagensCuidados
API do fabricanteAnthropic, OpenAI, Google, MistralModelos e recursos mais novos primeiro; documentação oficialDependência de um fornecedor; dados saem da sua infraestrutura
Nuvem que você já usaAmazon Bedrock, Google Vertex AI, Microsoft FoundryContrato, faturamento, rede privada e conformidade já resolvidos na empresaRecursos podem chegar depois; preço e cotas podem diferir
Modelos abertosLlama, Qwen, Mistral, DeepSeek, Gemma (hospedados por você ou por terceiros)Controle total dos dados; custo previsível em alto volume; ajuste fino livreVocê opera a infraestrutura; em geral, menos capacidade que os maiores modelos fechados

Os critérios de escolha

  1. Qualidade na sua tarefa. Rankings públicos ajudam a fazer a primeira lista, mas o que vale é o resultado nos seus casos reais. Esse é o tema do Módulo 3.
  2. Custo por tarefa concluída. Não por token: um modelo barato que precisa de três tentativas pode sair mais caro que um caro que acerta de primeira.
  3. Latência. O tempo até o primeiro token importa para chat; o tempo total importa para processamento em segundo plano.
  4. Janela de contexto e saída máxima. Quanto texto cabe na entrada e quanto o modelo pode escrever.
  5. Recursos da API. Saída estruturada, ferramentas, visão, cache, batch, controle de raciocínio.
  6. Dados e conformidade. Retenção de dados, região de processamento, uso para treino, LGPD, contrato.

A família de modelos de um provedor

Os provedores costumam oferecer tamanhos diferentes. Nos labs usamos a família Claude como referência (preços de nov/2026, por milhão de tokens):

ModeloEntrada / saídaBom para
claude-opus-5-5$4 / $20Raciocínio difícil, agentes, código complexo
claude-sonnet-5-5$2 / $10Uso geral, código do dia a dia, agentes rápidos
claude-haiku-5-5$0,10 / $0,50Classificação, extração e roteamento em alto volume
Estratégia prática Comece com o modelo mais capaz para provar que a tarefa é possível e criar um conjunto de testes. Depois teste modelos menores e esforço mais baixo contra esses testes. Fique com o mais barato que passa. Começar pelo mais barato faz você descartar ideias boas por acreditar que "IA não resolve".

Quiz rápido

Um hospital quer usar LLM com prontuários e a TI exige que os dados não saiam da nuvem contratada. A primeira opção a avaliar é…

Requisitos de dados e contrato eliminam opções antes da qualidade. Quase sempre há um caminho dentro da nuvem da empresa.

Qual métrica de custo leva às melhores decisões?

Tentativas extras, revisões humanas e tokens de raciocínio entram na conta. É o custo por resultado que importa.

2.2 Anatomia de uma requisição

Leitura + lab · 2h
resposta = client.messages.create(
    model="claude-opus-5-5",            # qual modelo
    max_tokens=8000,                    # teto da resposta (inclui raciocínio)
    system="Você é um mentor...",       # quem o modelo é, regras, contexto fixo
    messages=[                          # a conversa até agora
        {"role": "user", "content": "O que é idempotência?"},
        {"role": "assistant", "content": "É quando..."},
        {"role": "user", "content": "Me dá um exemplo em HTTP."},
    ],
    output_config={"effort": "low"},    # quanto raciocinar
)
CampoPara que serveDica
systemPapel, regras, formato, conhecimento fixoDeixe estável entre chamadas (ajuda o cache, aula 2.7)
messagesHistórico alternando user e assistantA API não tem memória: o histórico é responsabilidade sua
max_tokensLimite da respostaBaixo demais corta a resposta no meio; o raciocínio também conta
effortProfundidade do raciocínio: low a maxlow para tarefas simples; suba só se a qualidade medida pedir
toolsFerramentas que o modelo pode pedir para usarAula 2.5
output_config.formatForçar um JSON Schema na respostaAula 2.4

Os motivos de parada

Toda resposta traz um stop_reason. Seu código deve tratar cada um:

  • end_turn: terminou normalmente.
  • max_tokens: bateu no limite. A resposta está incompleta.
  • tool_use: o modelo quer usar uma ferramenta e espera o resultado.
  • refusal: o modelo recusou por política de segurança. Não use o conteúdo.
Recusas e modelo reserva Filtros de segurança às vezes recusam pedidos legítimos (falsos positivos). Nos labs de chat usamos o parâmetro fallbacks: "default" (beta), que faz o próprio servidor tentar um modelo alternativo quando isso acontece. Em qualquer provedor, trate a recusa como um caso previsto do fluxo, não como uma exceção rara.

Histórico: a conversa é uma lista que você mantém

Depois de cada resposta, adicione ao histórico a mensagem do assistente com todos os blocos que vieram (resposta.content), não só o texto. Isso preserva blocos de raciocínio e chamadas de ferramenta que o modelo precisa ver na próxima volta.

Repare que, como o histórico inteiro é reenviado a cada mensagem, o custo de entrada cresce a cada turno. Em conversas longas, isso domina a conta. Por isso existem o cache (aula 2.7) e a compactação de contexto (Módulo 6).

LAB 2.2 — Chat de terminal com histórico e custo

python python/01_chat_terminal.py
# ou
cd typescript && npx tsx 01-chat-terminal.ts
  1. Diga seu nome, converse por 3 turnos e pergunte "qual é meu nome?". Depois use /limpar e pergunte de novo.
  2. Observe a coluna de tokens de entrada a cada turno. Ela cresce como? Por quê?
  3. Faça a mesma pergunta difícil com ESFORCO=low e ESFORCO=high. Compare qualidade, tempo e tokens de saída.
  4. Altere o código para guardar só o texto da resposta no histórico, em vez de resposta.content. Por que isso é pior quando há ferramentas?

Quiz rápido

A resposta veio com stop_reason: "max_tokens". O que fazer?

Repetir sem mudar nada tende a cortar no mesmo ponto. Um JSON cortado, por exemplo, nem é parseável.

2.3 Prompt engineering e engenharia de contexto

Leitura + exercício · 3h

Prompt engineering é escrever bem a instrução. Engenharia de contexto é a disciplina mais ampla de decidir tudo o que entra na janela do modelo: instruções, documentos, histórico, resultados de ferramentas, exemplos. Com modelos de raciocínio, o segundo passou a importar mais que o primeiro: o modelo raramente erra por falta de "truque" no prompt, e sim por falta de informação ou por excesso de ruído.

Os princípios que funcionam

  1. Explique como para um colega novo e competente. Ele não conhece seu produto, seus usuários nem suas regras. Diga o objetivo, para quem é o resultado e o que é sucesso.
  2. Dê o porquê das regras. "Respostas curtas, porque aparecem numa notificação de celular" funciona melhor que "SEJA BREVE!!!". Com o motivo, o modelo generaliza para casos que você não previu.
  3. Separe instruções de dados. Coloque documentos e entradas do usuário entre tags (<email>…</email>). O modelo sabe onde começa e termina cada coisa, e fica mais difícil um texto malicioso se passar por instrução.
  4. Documentos longos primeiro, pergunta no fim. Em contextos grandes, a instrução logo depois do material costuma render melhor.
  5. Mostre exemplos quando o formato ou o tom for sutil. Dois ou três exemplos variados valem mais que um parágrafo de descrição. Cuidado: o modelo tende a copiar o exemplo de perto.
  6. Diga o que fazer, não só o que evitar. "Escreva em parágrafos corridos" é melhor que "não use listas".
  7. Permita a incerteza. "Se a informação não estiver no documento, diga que não encontrou" reduz alucinação.
  8. Não microgerencie o raciocínio. Com modelos que já raciocinam, receitas do tipo "pense passo a passo assim e assado" podem piorar o resultado. Dê o objetivo e os critérios, ajuste o esforço.

Antes e depois

Resuma esse texto. Seja breve!!! NÃO invente nada.
{texto}
Você resume chamados de suporte para o gerente do turno, que lê os resumos
no celular entre reuniões e decide quais escalar.

<chamado>
{texto}
</chamado>

Escreva um resumo de até 2 frases com: o problema do cliente e o que ele
pede. Se o cliente mencionar prazo, valor em dinheiro ou ameaça de cancelar,
inclua isso, porque é o que define a escalação. Use só o que está no
chamado; se algo estiver ambíguo, diga que está ambíguo.

Engenharia de contexto: menos e melhor

  • Cada token compete pela atenção do modelo. Despejar 50 documentos "por garantia" piora a resposta e o custo. Selecione o que é relevante (Módulo 4).
  • Resultados de ferramentas também são contexto. Uma ferramenta que devolve 10 mil linhas de JSON polui a janela. Devolva só os campos úteis.
  • Histórico envelhece. Em conversas longas, resuma ou descarte partes antigas (Módulo 6).
  • O prompt é código. Versione, revise em PR e teste (Módulo 3).

Exercício — Reescreva o prompt

Este prompt gera respostas inconsistentes num app de receitas. Reescreva-o aplicando os princípios da aula:

Você é um chef. Responda perguntas de culinária. Seja útil. Não fale de
outros assuntos. Use markdown.
Ver uma versão possível
Você é o assistente do app Cozinha Fácil, usado por pessoas que cozinham
em casa, muitas iniciantes, geralmente pelo celular com as mãos ocupadas.

Ajude com receitas, substituição de ingredientes, técnicas e planejamento
de refeições. Prefira ingredientes fáceis de achar em mercados brasileiros.
Quando der uma receita, liste ingredientes com quantidades e depois passos
numerados e curtos, porque o usuário segue lendo enquanto cozinha.

Se a pergunta envolver segurança alimentar (ponto de carne, conservação,
alergias), seja preciso e conservador: um erro aqui pode fazer mal a alguém.

Se pedirem algo fora de culinária, diga gentilmente que você só ajuda com
cozinha e sugira uma pergunta relacionada.

Quiz rápido

Por que colocar o e-mail do cliente entre tags <email>?

Sem delimitação, uma frase como "ignore as regras acima" dentro do e-mail se mistura com as suas instruções.

Seu RAG coloca 40 documentos no contexto e as respostas pioraram. A causa mais provável é…

Mais contexto não é melhor contexto. Selecionar e ordenar o que entra é o centro da engenharia de contexto.

2.4 Saídas estruturadas: dados, não texto

Leitura + lab · 3h

A maior parte das integrações não quer um parágrafo: quer um objeto para salvar no banco, rotear um chamado ou preencher um formulário. Pedir "responda só em JSON" no prompt funciona quase sempre, e "quase sempre" é o pior tipo de bug em produção.

Com saídas estruturadas, você envia um JSON Schema e a API restringe a geração para que a resposta siga esse schema. Os SDKs geram o schema a partir dos tipos que você já usa: Pydantic em Python e Zod em TypeScript.

const Chamado = z.object({
  categoria: z.enum(["cobranca", "bug", "duvida", "cancelamento", "elogio", "outro"]),
  prioridade: z.enum(["baixa", "media", "alta", "critica"])
    .describe("critica = sistema fora do ar ou perda de dinheiro"),
  resumo: z.string().describe("Uma frase, no máximo 20 palavras"),
  pede_reembolso: z.boolean(),
});

const resposta = await client.messages.parse({
  model: "claude-opus-5-5",
  max_tokens: 4000,
  messages: [{ role: "user", content: `<email>${texto}</email>\nExtraia o chamado.` }],
  output_config: { effort: "low", format: zodOutputFormat(Chamado) },
});

const chamado = resposta.parsed_output; // tipado: chamado.prioridade é "baixa" | "media" | ...

Boas práticas de schema

  • .describe() / Field(description=…) é prompt. Explique o critério de cada campo, principalmente os subjetivos.
  • Use enums para categorias. Texto livre gera "Cobrança", "cobranca", "financeiro"…
  • Campos opcionais como null explícito (.nullable(), str | None), para o modelo não inventar valor quando a informação não existe.
  • Inclua campos de autoavaliação como confianca ou observacoes. Eles alimentam a revisão humana.
  • Valide de novo no seu código. Os SDKs validam o formato com Zod/Pydantic, mas nem toda restrição cabe no schema enviado à API (o SDK pode, por exemplo, passar opções de enum ou limites numéricos para a descrição do campo). E nenhuma restrição de schema garante que o conteúdo está certo: isso é com as suas regras de negócio (projeto do módulo).

LAB 2.4 — De e-mail a chamado tipado

python python/02_saida_estruturada.py                          # e-mail irritado
python python/02_saida_estruturada.py dados/email_cliente_2.txt  # elogio + dúvida
# ou
cd typescript && npx tsx 02-saida-estruturada.ts
  1. Rode com os dois e-mails. A prioridade e a categoria fazem sentido?
  2. O primeiro e-mail tem dois assuntos (cobrança e bug). Como o schema lida com isso? Mude-o para aceitar uma lista de categorias.
  3. Crie um terceiro e-mail ambíguo e veja como o modelo preenche nome_cliente quando o nome não aparece.

Quiz rápido

Com saída estruturada, o que ainda pode dar errado?

O schema garante a forma, não a verdade. Por isso existem validações de negócio e evals.

2.5 Tool calling: o modelo pede, seu código age

Leitura + lab · 4h

Um LLM sozinho não consulta seu banco, não sabe a cotação de hoje e não envia e-mail. Com ferramentas (tools, ou function calling), você descreve funções que ele pode pedir para usar. O ciclo:

1. Você envia a pergunta + a lista de ferramentas→ 2. O modelo responde tool_use: "chame X com estes argumentos"→ 3. Seu código executa X→ 4. Você devolve o tool_result↺

O loop se repete até o modelo responder sem pedir ferramenta (stop_reason diferente de tool_use). O modelo nunca executa nada: ele só pede. Quem decide executar, e com quais permissões, é o seu código. Essa é a base de segurança de todo agente.

{
    "name": "calcular_frete",
    "description": "Calcula valor e prazo de frete para um CEP de destino e um peso em kg.",
    "strict": True,   # garante que os argumentos seguem o schema
    "input_schema": {
        "type": "object",
        "properties": {
            "cep": {"type": "string", "description": "CEP de destino, com ou sem hífen"},
            "peso_kg": {"type": "number"},
        },
        "required": ["cep", "peso_kg"],
        "additionalProperties": False,
    },
}

Regras de ouro

  • A descrição é um prompt. Diga o que a ferramenta faz, quando usar e o que devolve. Nomes e descrições ruins são a causa nº 1 de ferramenta errada.
  • Erros voltam para o modelo como tool_result com is_error: true. Ele pode corrigir o argumento ou explicar o problema ao usuário.
  • Vários pedidos na mesma resposta são comuns (chamadas paralelas). Devolva todos os resultados juntos, em uma única mensagem.
  • Sempre ponha um teto no loop. Sem limite de rodadas, um modelo confuso pode chamar ferramentas para sempre, e você paga por cada volta.
  • Ações com efeito no mundo real (pagar, apagar, enviar) precisam de confirmação humana ou de regras rígidas no seu código. Detalhes nos Módulos 6 e 12.

LAB 2.5 — Atendente com duas ferramentas

python python/03_ferramentas.py
python python/03_ferramentas.py "Qual o status do pedido 9999?"
# ou
cd typescript && npx tsx 03-ferramentas.ts

O log mostra cada pedido do modelo e cada resultado. Experimentos:

  1. Pergunte sobre um pedido inexistente. Como o modelo reage ao erro da ferramenta?
  2. Pergunte o frete de dois pedidos ao mesmo tempo. O modelo faz chamadas em paralelo?
  3. Piore de propósito a descrição de calcular_frete (por exemplo, "faz cálculos"). O que muda?
  4. Adicione uma ferramenta cancelar_pedido que só executa depois de o usuário digitar "sim" no terminal.

Quiz rápido

Quem executa a função calcular_frete?

O modelo só gera o pedido estruturado. Execução, permissões e validação ficam com você.

A ferramenta falhou com "CEP deve ter 8 dígitos". O que devolver ao modelo?

Com o erro, o modelo pode pedir o CEP correto ao usuário. Inventar um valor esconderia o problema.

2.6 Streaming: a resposta enquanto ela nasce

Leitura + lab · 1h30

Uma resposta de 800 tokens pode levar dezenas de segundos para ficar pronta. Sem streaming, o usuário encara uma tela parada. Com streaming, o texto começa a aparecer em cerca de um segundo. O tempo total é o mesmo; a percepção muda completamente.

const stream = client.messages.stream({ model, max_tokens: 16000, messages });

stream.on("text", (pedaco) => process.stdout.write(pedaco)); // cada pedaço, assim que chega

const final = await stream.finalMessage(); // mensagem completa: usage, stop_reason

Quando usar

  • Sempre que uma pessoa estiver esperando a resposta (chat, assistentes, editores).
  • Em respostas longas, mesmo sem pessoa esperando: streaming evita que a conexão HTTP estoure o tempo limite.
  • Não precisa em jobs de fundo curtos, nem em extração estruturada em que você só usa o objeto final.

Duas métricas para acompanhar

  • TTFT (time to first token): o tempo até o primeiro pedaço. Define a sensação de rapidez.
  • Tempo total: define quanto tempo a conexão fica aberta e o throughput do sistema.
No navegador Em apps web, o back-end recebe o stream do provedor e o repassa ao front-end por Server-Sent Events ou WebSocket. A chave de API nunca vai para o navegador. Você vai construir isso no Módulo 8.

LAB 2.6 — Medindo TTFT e tempo total

cd typescript && npx tsx 04-streaming.ts
npx tsx 04-streaming.ts "Escreva um conto de 400 palavras sobre um robô que aprende a cozinhar"
  1. Compare TTFT e tempo total entre uma resposta curta e uma longa.
  2. Rode com esforço high numa pergunta difícil. O que acontece com o TTFT? Por quê?

Quiz rápido

Streaming reduz o custo da chamada?

O custo é o mesmo. Streaming é sobre experiência e robustez da conexão.

2.7 Cache de prompt, batch e economia de tokens

Leitura + labs · 3h

Cache de prompt

Muitas aplicações reenviam o mesmo início a cada chamada: instruções longas, um manual, o histórico da conversa. Com cache de prompt, o provedor guarda esse prefixo processado por alguns minutos, e as próximas chamadas que começam igual pagam uma fração do preço por ele, além de responderem mais rápido.

SISTEMA = [
    {"type": "text", "text": "Responda usando SOMENTE o manual abaixo."},
    {"type": "text", "text": f"<manual>{manual}</manual>",
     "cache_control": {"type": "ephemeral"}},   # tudo até aqui vai para o cache
]
OperaçãoPreço relativo à entrada normal
Gravar no cache (validade de 5 min)1,25×
Gravar no cache (validade de 1 h)2×
Ler do cachecerca de 0,1× (no claude-opus-5-5, 0,05×)

As regras que fazem o cache funcionar, ou falhar em silêncio:

  • É uma comparação de prefixo. Qualquer byte diferente antes do ponto de cache invalida tudo depois dele. Coloque o que é estável primeiro e o que varia (a pergunta) por último.
  • Inimigos silenciosos: data e hora no prompt de sistema, IDs de requisição, JSON com chaves em ordem diferente, lista de ferramentas que muda.
  • Há um tamanho mínimo (centenas a alguns milhares de tokens, conforme o modelo). Abaixo dele o cache simplesmente não acontece, sem erro.
  • Confirme sempre pelo usage: cache_creation_input_tokens (gravou) e cache_read_input_tokens (leu). Se a leitura fica em zero, algo está mudando o prefixo.

Batch: metade do preço para o que pode esperar

A API de lotes recebe milhares de requisições de uma vez, processa em segundo plano (em geral em menos de uma hora, no máximo em 24 h) e cobra 50% menos. É ideal para classificar o acumulado do dia, gerar descrições de catálogo, enriquecer dados e rodar evals.

  • Cada requisição leva um custom_id. Os resultados chegam fora de ordem: case sempre pelo ID.
  • Algumas requisições podem falhar e outras não. Trate cada resultado individualmente.

Outras formas de economizar tokens

  • Modelo certo por tarefa (aula 2.1): de longe a maior alavanca.
  • Esforço mínimo que mantém a qualidade. Tokens de raciocínio são cobrados como saída.
  • Peça saídas enxutas. Saída custa 5× a entrada. Um JSON com campos curtos é mais barato que um parágrafo.
  • Corte o contexto inútil: HTML com marcação, logs inteiros, histórico velho.
  • Conte antes de enviar documentos grandes com a API de contagem de tokens.

LAB 2.7a — Cache em ação

python python/04_cache.py

São três perguntas sobre o mesmo manual (dados/manual_produto.md, com bem mais que o mínimo de tokens para cache). Verifique: a 1ª chamada grava no cache e a 2ª e a 3ª leem dele. O script calcula a economia.

  1. Coloque datetime.now() no primeiro bloco do sistema e rode de novo. O que acontece com o cache?
  2. Mova a pergunta para dentro do sistema, antes do manual. Por que o cache para de funcionar?

LAB 2.7b — Classificação em lote

python python/05_batch.py
python python/05_batch.py msgbatch_...   # para retomar um lote já enviado

Oito avaliações de clientes (dados/avaliacoes.jsonl) são classificadas por sentimento e tema, com saída estruturada dentro do lote. Repare que o script imprime na ordem em que os resultados chegam, não na ordem do arquivo.

Quiz rápido

Seu prompt de sistema começa com "Hoje é 01/11/2026 14:32:07". O cache nunca é lido. Por quê?

Se precisar da data, coloque-a depois do ponto de cache (na mensagem do usuário), e com precisão de dia, não de segundo.

Qual tarefa é boa candidata para batch?

Batch troca latência por preço: serve para o que ninguém está esperando na hora.

2.8 Erros, retries, timeouts e limites de taxa

Leitura + lab · 1h30

Uma API de LLM é um serviço remoto como qualquer outro: cai, fica lenta e limita o uso. Trate-a com as mesmas práticas de sistemas distribuídos.

ErroSignificadoTentar de novo?
400Requisição inválida (parâmetro errado, schema inválido)Não: corrija o código
401 / 403Chave inválida ou sem permissãoNão
404Modelo ou recurso inexistenteNão
429Limite de taxa (requisições ou tokens por minuto)Sim, com espera crescente
500 / 529Erro ou sobrecarga do provedorSim, com espera crescente
Timeout / conexãoRede ou resposta lenta demaisSim, com cuidado

O que o SDK já faz por você

Os SDKs oficiais repetem automaticamente erros de conexão, 408, 409, 429 e 5xx, com espera exponencial (2 tentativas por padrão). Configure, não reimplemente:

const client = new Anthropic({ maxRetries: 3, timeout: 60_000 }); // ms no SDK TypeScript

// por chamada:
await client.messages.create(params, { timeout: 10_000, maxRetries: 0 });

O que continua sendo seu

  • Classificar o erro pelos tipos do SDK (RateLimitError, BadRequestError…), nunca comparando o texto da mensagem.
  • Idempotência: se a chamada dispara uma ação (enviar e-mail), garanta que uma repetição não a duplique.
  • Degradação elegante: o que o usuário vê se a IA estiver fora? Uma mensagem honesta, uma fila para depois, um modelo reserva.
  • Limites de taxa no seu lado: controle a concorrência dos seus jobs (filas, semáforos) para não viver no 429.
  • Timeouts realistas: com raciocínio ativo, respostas boas podem levar minutos. Use streaming nas chamadas longas.

LAB 2.8 — Provocando erros de propósito

cd typescript && npx tsx 05-erros-e-limites.ts

O script faz uma chamada normal, uma com modelo inexistente, uma com max_tokens minúsculo e uma com timeout de 1 ms, e mostra como cada uma é classificada.

  1. Adicione um caso com uma chave de API inválida (dica: crie outro client com apiKey: "x").
  2. Escreva uma função comFallback(params) que tenta claude-opus-5-5 e, em caso de 429 ou 5xx, tenta claude-sonnet-5-5.

2.9 Vários provedores: gateways e abstrações

Leitura · 1h

Cedo ou tarde surge a pergunta: "e se quisermos trocar de provedor?". Há três abordagens, com custos diferentes:

AbordagemComoPrósContras
Gateway de terceirosOpenRouter e serviços similares: uma API, centenas de modelosExperimentar rápido; um só faturamentoMais um intermediário vendo seus dados; recursos avançados nem sempre passam
Biblioteca/proxy próprioLiteLLM ou similar rodando na sua infraestruturaControle; registro centralizado de uso e custoFormato "mínimo denominador comum"; mais um serviço para operar
Interface própria finaUma função do seu domínio com adaptadores por provedorUsa o melhor de cada SDK; fácil de testarVocê escreve os adaptadores

Para a maioria das aplicações, a terceira opção é a mais saudável. Seu código depende de uma interface do seu domínio, não do formato de nenhum provedor:

// o resto do sistema só conhece isto:
interface ExtratorDeRecibos {
  extrair(texto: string): Promise<{ recibo: Recibo; custoUsd: number }>;
}

// e cada provedor vira um adaptador, usando os recursos nativos do seu SDK:
class ExtratorAnthropic implements ExtratorDeRecibos { /* messages.parse + zod */ }
class ExtratorModeloLocal implements ExtratorDeRecibos { /* servidor local */ }
O que não se transfere Trocar de provedor não é só trocar a URL. Prompts são ajustados para um modelo, recursos (cache, raciocínio, ferramentas de servidor) variam e a qualidade muda. Toda troca precisa passar pelos seus evals antes de ir para produção (Módulo 3).

Quiz rápido

Qual a principal razão para ter evals antes de trocar de modelo ou provedor?

É exatamente o tema do próximo módulo.

Projeto do módulo: extrator de recibos

Projeto · com código 8–10h · com agentes 5–6h

Um sistema que lê notas e cupons fiscais em texto (como os que saem de um OCR), devolve dados estruturados, valida com regras de negócio, pede ao modelo que revise quando a validação falha e gera um relatório de qualidade e custo. É o padrão de quase toda automação séria com LLM: extrair → validar → corrigir → revisar humano quando necessário.

Este projeto existe nas duas trilhas. O objetivo, o critério de pronto e os recibos são os mesmos; muda quem escreve o código. Na trilha com código, você escreve. Na trilha com agentes, o Claude escreve e você decide, confere e aprende como o problema se resolve (veja o P3).

Objetivo e critério de pronto (iguais nas duas trilhas)

Para cada recibo da pasta recibos/, devolver emitente e CNPJ, data, itens, desconto, acréscimo, total e forma de pagamento. Está pronto quando:

  1. Padaria, farmácia e restaurante passam em todas as regras: CNPJ válido, data válida e não futura, quantidade × preço de cada item e soma dos itens − desconto + acréscimo = total.
  2. O recibo do mercado, cuja soma não fecha na própria nota, sai com os valores impressos e confianca: "baixa", para revisão humana. Ajustar os números para a conta fechar é erro.
  3. As regras de conferência têm testes automáticos, e eles passam sem chamar a API.
  4. O custo por recibo é conhecido, e há uma recomendação de modelo para 10 mil recibos por mês, justificada com números.

Os recibos de exemplo

  • 01-padaria.txt e 02-farmacia.txt: limpos; o segundo tem desconto.
  • 03-mercado-foto.txt: simula OCR de foto, com letras trocadas por números (OOO1, 1O/2026), e uma soma que não fecha na própria nota.
  • 04-restaurante.txt: tem taxa de serviço (acréscimo).

Por que o mercado importa: padaria, farmácia e restaurante devem passar na validação. No recibo do mercado, a regra de soma deve disparar; o modelo recebe o problema, relê a nota e, seguindo as instruções, mantém os valores impressos com confianca: "baixa", em vez de "ajeitar" os números para a conta fechar. É exatamente o comportamento que você quer num sistema contábil: o caso vai para revisão humana.

Trilha com código

cd projeto
npm install
npm test                      # testes das regras de validação e custo (não chamam a API)
npm run extrair               # processa recibos/*.txt
npm run extrair -- --modelo claude-haiku-5-5

Arquitetura

ArquivoResponsabilidade
src/esquema.tsO contrato (Zod): emitente, data, itens, desconto, acréscimo, total, pagamento, confiança, observações
src/validar.tsRegras que o schema não expressa: dígitos do CNPJ, data válida e não futura, quantidade × unitário, soma dos itens
src/extrator.tsChamada com saída estruturada e o loop "validar e corrigir": se a validação falha, os problemas voltam ao modelo uma vez
src/custo.tsCusto por chamada, incluindo cache e desconto de batch
src/cli.tsProcessa a pasta, grava saida/*.json e imprime o relatório
test/14 testes com node:test, que rodam sem chave de API

Entregáveis

  1. Adicione 6 recibos seus (anonimizados), incluindo pelo menos um com erro proposital.
  2. Adicione uma regra de validação nova (por exemplo, data anterior a 5 anos é suspeita) com testes.
  3. Implemente um modo --batch que envia todos os recibos pela API de lotes e compare o custo.
  4. Rode com claude-opus-5-5 e claude-haiku-5-5. Monte uma tabela com taxa de aprovação, tentativas, tempo e custo por recibo.
  5. No README, recomende um modelo para 10 mil recibos por mês, justificando com os seus números.

Critérios de avaliação

CritérioPeso
Extração correta e validação funcionando nos recibos novos30%
Nova regra de validação com testes15%
Modo batch funcionando20%
Comparação de modelos com números reais20%
README e recomendação justificada15%

Trilha com agentes

Você vai conduzir o Claude Code até o mesmo resultado, sem escrever código, em cinco momentos (P3.7). Antes de começar: Claude Code configurado com o plugin autonomo e a variável ANTHROPIC_API_KEY definida (Módulo 1, Passo 1).

Momento 1 · O problema, antes de ver a solução

Leia o objetivo e os recibos acima. Escreva, em cinco linhas, como você acha que um programa resolveria isso. Onde ele pode errar? Guarde: você vai comparar no fim.

Momento 2 · A resolução comentada

Crie o projeto e entregue o pedido. No Claude Code:

/autonomo:novo-projeto extrator-de-recibos

Copie os 4 recibos de curso-ia/codigo/modulo-02/projeto/recibos do repositório
MarciodeCampos/o-arco para a pasta recibos/ deste projeto.

Quero: um programa que leia cada recibo da pasta recibos/ e devolva emitente com CNPJ,
data, itens (quantidade, preço unitário, total), desconto, acréscimo, total e pagamento.
Para: conferir recibos sem digitar nada à mão.
Pronto quando:
  1. Padaria, farmácia e restaurante passam nas regras: CNPJ válido, data válida e não
     futura, quantidade × preço de cada item, e itens − desconto + acréscimo = total.
  2. O recibo do mercado, cuja soma não fecha na própria nota, sai com os valores
     impressos e confiança baixa, para revisão humana. Não ajuste os números.
  3. As regras de conferência têm testes automáticos que rodam sem chamar a API.
  4. Você me mostra o custo por recibo.
Não pode: ler ou mostrar a chave de API (ela já está em ANTHROPIC_API_KEY).
Antes de cada etapa importante, me mostre o que propõe, por quê, as alternativas e o
risco, e espere minha autorização. Narre cada decisão; eu estou aprendendo como se resolve.

A seguir, o roteiro do que costuma acontecer, com os pontos de previsão (◆). Em cada um, pare e escreva o que você acha que vem a seguir antes de autorizar. O Claude pode propor em outra ordem ou com outros nomes; o que importa são as decisões.

  1. Linguagem e estrutura. O Claude propõe TypeScript ou Python e uma pasta com o código, os testes e os recibos. Decisão de baixo risco: autorize.
  2. ◆ Como garantir que a resposta venha no formato certo? Ele deve propor saída estruturada: um esquema com os campos e os tipos, que a API obriga o modelo a seguir. A alternativa ingênua é pedir "responda em JSON" e torcer.
  3. ◆ Onde ficam as regras da conta? Esta é a decisão central do projeto. O Claude deve propor conferir CNPJ, data e somas no código, depois da resposta do modelo, e não só pedir ao modelo para conferir. O modelo pode devolver um JSON perfeito com a conta errada; o código não erra uma soma.
  4. Os testes das regras. Antes de chamar a API, ele escreve testes com recibos inventados: um certo, um com CNPJ inválido, um com soma errada. Peça para ver um teste falhando de propósito (P3.4).
  5. ◆ O que fazer quando a regra falha? Ele deve propor devolver os problemas ao modelo uma vez, para que releia a nota. Se ainda não fechar, o recibo vai para revisão humana com confiança baixa. Tentar indefinidamente gasta dinheiro e pode empurrar o modelo a inventar números.
  6. A primeira execução. Aqui o programa chama a API: o Claude pede autorização, porque gasta créditos (centavos). Peça o resultado em tabela: recibo, passou nas regras, confiança, observações.
  7. ◆ Qual modelo usar em produção? Ele propõe rodar com dois modelos (um maior e um menor) e comparar taxa de aprovação, tempo e custo por recibo. Bom momento para pedir uma segunda opinião (P3.6) sobre a recomendação.

Momento 3 · Por quê

Responda com as suas palavras, sem consultar o Claude. Depois peça a ele para corrigir as suas respostas.

  1. Por que as regras da conta ficam no código e não no pedido ao modelo?
  2. Por que o recibo do mercado deve sair com confiança baixa, mesmo que o modelo consiga "consertar" o total?
  3. Por que os testes das regras não chamam a API?
  4. Por que devolver o problema ao modelo só uma vez?

Momento 4 · Ache o erro

Outra pessoa conduziu o mesmo projeto e entregou este relatório, com todos os testes passando. Há um erro grave. Qual é, e como você percebeu?

recibo               regras   confiança   observações
01-padaria           ok       alta
02-farmacia          ok       alta        desconto de 10% aplicado
03-mercado-foto      ok       alta        total corrigido de 89,11 para 82,11 para fechar a soma
04-restaurante       ok       alta        taxa de serviço de 10%

4 de 4 aprovados · custo médio US$ 0,0031 por recibo
Ver a resposta

O mercado foi "aprovado" porque o total impresso foi trocado para a conta fechar. Num sistema contábil isso esconde um problema da nota original: o recibo deveria ir para revisão humana com confiança baixa e os valores impressos. Pistas: a observação "corrigido para fechar a soma" e um 4 de 4 num conjunto que tem, de propósito, um recibo com erro. Testes passando não provam nada se nenhum teste confere esse caso: o que faltou foi um teste com um recibo cuja soma não fecha.

Momento 5 · Variação

Agora é você quem conduz, só com pedidos e autorizações. A mudança: recibos com data de mais de 5 anos são suspeitos e devem ir para revisão. Crie um quinto recibo com data antiga (copie um dos quatro e mude a data). Pronto quando o recibo antigo sai com confiança baixa, os outros quatro continuam como antes, e existe um teste novo para a regra que você viu falhar e depois passar. Registre as decisões no DECISOES.md.

Entregáveis

  1. O projeto no seu GitHub, feito pelo Claude, com os testes passando.
  2. Seis recibos seus (anonimizados), incluindo pelo menos um com erro proposital.
  3. O DECISOES.md com pelo menos cinco decisões, cada uma com o seu porquê, e uma delas com segunda opinião.
  4. Suas respostas dos momentos 1, 3 e 4, e o que mudou entre o que você previu no momento 1 e o que viu.
  5. A tabela com dois modelos (taxa de aprovação, tempo e custo por recibo) e, escrita por você, a recomendação para 10 mil recibos por mês.

Critérios de avaliação

CritérioPeso
Resultado correto nos recibos de exemplo e nos seus, incluindo o mercado em revisão25%
Registro de decisões: porquês claros, alternativas consideradas, segunda opinião usada com critério25%
Explicações do momento 3 e o erro encontrado no momento 420%
Variação conduzida até o fim, com teste novo15%
Recomendação de modelo justificada com os seus números15%
Ponte para o Módulo 3 Você vai perceber que comparar modelos olhando a saída no terminal não escala. No próximo módulo, este mesmo extrator ganha um conjunto de avaliação com respostas esperadas e uma nota automática, rodando no CI.

Recursos para aprofundar

Todos gratuitos