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.
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-..."
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
Há três formas de acessar um LLM em produção:
| Forma | Exemplos | Vantagens | Cuidados |
|---|---|---|---|
| API do fabricante | Anthropic, OpenAI, Google, Mistral | Modelos e recursos mais novos primeiro; documentação oficial | Dependência de um fornecedor; dados saem da sua infraestrutura |
| Nuvem que você já usa | Amazon Bedrock, Google Vertex AI, Microsoft Foundry | Contrato, faturamento, rede privada e conformidade já resolvidos na empresa | Recursos podem chegar depois; preço e cotas podem diferir |
| Modelos abertos | Llama, Qwen, Mistral, DeepSeek, Gemma (hospedados por você ou por terceiros) | Controle total dos dados; custo previsível em alto volume; ajuste fino livre | Você opera a infraestrutura; em geral, menos capacidade que os maiores modelos fechados |
Os critérios de escolha
- 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.
- 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.
- Latência. O tempo até o primeiro token importa para chat; o tempo total importa para processamento em segundo plano.
- Janela de contexto e saída máxima. Quanto texto cabe na entrada e quanto o modelo pode escrever.
- Recursos da API. Saída estruturada, ferramentas, visão, cache, batch, controle de raciocínio.
- 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):
| Modelo | Entrada / saída | Bom para |
|---|---|---|
claude-opus-5-5 | $4 / $20 | Raciocínio difícil, agentes, código complexo |
claude-sonnet-5-5 | $2 / $10 | Uso geral, código do dia a dia, agentes rápidos |
claude-haiku-5-5 | $0,10 / $0,50 | Classificação, extração e roteamento em alto volume |
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
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
)
| Campo | Para que serve | Dica |
|---|---|---|
system | Papel, regras, formato, conhecimento fixo | Deixe estável entre chamadas (ajuda o cache, aula 2.7) |
messages | Histórico alternando user e assistant | A API não tem memória: o histórico é responsabilidade sua |
max_tokens | Limite da resposta | Baixo demais corta a resposta no meio; o raciocínio também conta |
effort | Profundidade do raciocínio: low a max | low para tarefas simples; suba só se a qualidade medida pedir |
tools | Ferramentas que o modelo pode pedir para usar | Aula 2.5 |
output_config.format | Forçar um JSON Schema na resposta | Aula 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.
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
- Diga seu nome, converse por 3 turnos e pergunte "qual é meu nome?". Depois use
/limpare pergunte de novo. - Observe a coluna de tokens de entrada a cada turno. Ela cresce como? Por quê?
- Faça a mesma pergunta difícil com
ESFORCO=loweESFORCO=high. Compare qualidade, tempo e tokens de saída. - 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
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
- 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.
- 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.
- 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. - Documentos longos primeiro, pergunta no fim. Em contextos grandes, a instrução logo depois do material costuma render melhor.
- 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.
- Diga o que fazer, não só o que evitar. "Escreva em parágrafos corridos" é melhor que "não use listas".
- Permita a incerteza. "Se a informação não estiver no documento, diga que não encontrou" reduz alucinação.
- 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
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
nullexplícito (.nullable(),str | None), para o modelo não inventar valor quando a informação não existe. - Inclua campos de autoavaliação como
confiancaouobservacoes. 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
- Rode com os dois e-mails. A prioridade e a categoria fazem sentido?
- 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.
- Crie um terceiro e-mail ambíguo e veja como o modelo preenche
nome_clientequando 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
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:
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_resultcomis_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:
- Pergunte sobre um pedido inexistente. Como o modelo reage ao erro da ferramenta?
- Pergunte o frete de dois pedidos ao mesmo tempo. O modelo faz chamadas em paralelo?
- Piore de propósito a descrição de
calcular_frete(por exemplo, "faz cálculos"). O que muda? - Adicione uma ferramenta
cancelar_pedidoque 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
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.
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"
- Compare TTFT e tempo total entre uma resposta curta e uma longa.
- Rode com esforço
highnuma 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
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ção | Preço relativo à entrada normal |
|---|---|
| Gravar no cache (validade de 5 min) | 1,25× |
| Gravar no cache (validade de 1 h) | 2× |
| Ler do cache | cerca 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) ecache_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.
- Coloque
datetime.now()no primeiro bloco do sistema e rode de novo. O que acontece com o cache? - 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
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.
| Erro | Significado | Tentar de novo? |
|---|---|---|
| 400 | Requisição inválida (parâmetro errado, schema inválido) | Não: corrija o código |
| 401 / 403 | Chave inválida ou sem permissão | Não |
| 404 | Modelo ou recurso inexistente | Não |
| 429 | Limite de taxa (requisições ou tokens por minuto) | Sim, com espera crescente |
| 500 / 529 | Erro ou sobrecarga do provedor | Sim, com espera crescente |
| Timeout / conexão | Rede ou resposta lenta demais | Sim, 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.
- Adicione um caso com uma chave de API inválida (dica: crie outro client com
apiKey: "x"). - Escreva uma função
comFallback(params)que tentaclaude-opus-5-5e, em caso de 429 ou 5xx, tentaclaude-sonnet-5-5.
2.9 Vários provedores: gateways e abstrações
Cedo ou tarde surge a pergunta: "e se quisermos trocar de provedor?". Há três abordagens, com custos diferentes:
| Abordagem | Como | Prós | Contras |
|---|---|---|---|
| Gateway de terceiros | OpenRouter e serviços similares: uma API, centenas de modelos | Experimentar rápido; um só faturamento | Mais um intermediário vendo seus dados; recursos avançados nem sempre passam |
| Biblioteca/proxy próprio | LiteLLM ou similar rodando na sua infraestrutura | Controle; registro centralizado de uso e custo | Formato "mínimo denominador comum"; mais um serviço para operar |
| Interface própria fina | Uma função do seu domínio com adaptadores por provedor | Usa o melhor de cada SDK; fácil de testar | Você 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 */ }
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
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).
Para cada recibo da pasta recibos/, devolver emitente e CNPJ, data, itens, desconto, acréscimo, total e forma de pagamento. Está pronto quando:
- 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.
- 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. - As regras de conferência têm testes automáticos, e eles passam sem chamar a API.
- 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.txte02-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
| Arquivo | Responsabilidade |
|---|---|
src/esquema.ts | O contrato (Zod): emitente, data, itens, desconto, acréscimo, total, pagamento, confiança, observações |
src/validar.ts | Regras 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.ts | Chamada com saída estruturada e o loop "validar e corrigir": se a validação falha, os problemas voltam ao modelo uma vez |
src/custo.ts | Custo por chamada, incluindo cache e desconto de batch |
src/cli.ts | Processa a pasta, grava saida/*.json e imprime o relatório |
test/ | 14 testes com node:test, que rodam sem chave de API |
Entregáveis
- Adicione 6 recibos seus (anonimizados), incluindo pelo menos um com erro proposital.
- Adicione uma regra de validação nova (por exemplo, data anterior a 5 anos é suspeita) com testes.
- Implemente um modo
--batchque envia todos os recibos pela API de lotes e compare o custo. - Rode com
claude-opus-5-5eclaude-haiku-5-5. Monte uma tabela com taxa de aprovação, tentativas, tempo e custo por recibo. - No README, recomende um modelo para 10 mil recibos por mês, justificando com os seus números.
Critérios de avaliação
| Critério | Peso |
|---|---|
| Extração correta e validação funcionando nos recibos novos | 30% |
| Nova regra de validação com testes | 15% |
| Modo batch funcionando | 20% |
| Comparação de modelos com números reais | 20% |
| README e recomendação justificada | 15% |
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.
- 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.
- ◆ 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.
- ◆ 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.
- 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).
- ◆ 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.
- 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.
- ◆ 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.
- Por que as regras da conta ficam no código e não no pedido ao modelo?
- Por que o recibo do mercado deve sair com confiança baixa, mesmo que o modelo consiga "consertar" o total?
- Por que os testes das regras não chamam a API?
- 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
- O projeto no seu GitHub, feito pelo Claude, com os testes passando.
- Seis recibos seus (anonimizados), incluindo pelo menos um com erro proposital.
- O
DECISOES.mdcom pelo menos cinco decisões, cada uma com o seu porquê, e uma delas com segunda opinião. - Suas respostas dos momentos 1, 3 e 4, e o que mudou entre o que você previu no momento 1 e o que viu.
- 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ério | Peso |
|---|---|
| Resultado correto nos recibos de exemplo e nos seus, incluindo o mercado em revisão | 25% |
| Registro de decisões: porquês claros, alternativas consideradas, segunda opinião usada com critério | 25% |
| Explicações do momento 3 e o erro encontrado no momento 4 | 20% |
| Variação conduzida até o fim, com teste novo | 15% |
| Recomendação de modelo justificada com os seus números | 15% |
Recursos para aprofundar
- Documentação da API da Anthropic · guias de prompts, saídas estruturadas, ferramentas, cache e batch
- Effective context engineering for AI agents · artigo sobre engenharia de contexto
- Building effective agents · quando usar chamada única, workflow ou agente (ponte para o Módulo 6)
- Zod e Pydantic · as bibliotecas de schema usadas nos labs
- OpenRouter e LiteLLM · para experimentar vários provedores