Você vai precisar deste guia antes de escrever outro import openai
As IAs estão em todo lugar e dentro dos seus sistemas. Se você construiu um app que chama um LLM, já caiu (ou vai cair) nestes buracos: a API de um provider mudou e o código quebrou; um usuário descobriu um jeito de torrar seu crédito; ou o modelo respondeu uma coisa na sexta e outra na segunda, sem que nada mudasse no input.
Este é um guia. Nove princípios separam quem só pluga um SDK de quem desenha um sistema que aguenta produção. Cada princípio tem a mesma estrutura: o problema, o mecanismo, o trade-off, o como fazer, o exemplo e a validação. Ao final, um checklist para o deploy, e o critério de "quando" cada coisa é obrigatória (MVP vs. produção).
Princípio 1. Abstraia o provedor, ou o provedor vira dono do seu código
O problema: você espalhou import openai (ou anthropic) pela codebase. Funciona no primeiro dia; no dia em que precisar trocar de provider, você reescreve quase tudo. Um founder fez exatamente isso, o sistema inteiro preso a um SDK, e o código virou refém de um único fornecedor.
O mecanismo: ports and adapters. Uma interface (port) declara as operações que você usa (chat, complete, callTool). Um adaptador por provedor traduz essa interface para a API nativa de cada um. A aplicação recebe o adaptador por injeção de dependência e chama apenas o contrato.
O trade-off: a abstração deve capturar só o que seu sistema realmente usa. Abstrair demais, por precaução, esconde capacidades específicas (structured output nativo, tool use e outra) e adiciona manutenção. Abstrair de menos deixa o provider vazar no core. Desenhe o port o mais fino que o seu uso atual exige, e amplie quando houver evidência de necessidade.
Cuidado com nomenclatura: functionCall é termo da OpenAI; Claude usa tool use, Gemini usa function declarations. O contrato deve descrever a intenção (callTool(name, args)), e cada adaptador traduz.
Como fazer:
- Crie o port com as operações de fato usadas.
- Implemente um adaptador por provedor.
- Injete o adaptador; a codebase chama o contrato, nunca o SDK.
- Trocar de provider = trocar a dependência injetada.
Validação: teste capaz de trocar um adaptador fake (sem rede) por um real e ver o core passar inalterado. Um smoke test com dois providers provando que o mesmo fluxo roda = o port está no ponto certo.
Princípio 2. Colapse a resposta no determinístico e valide o formato
O problema: LLMs não são determinísticas. O mesmo input pode gerar outputs diferentes. Seu sistema precisa tomar decisão sobre respostas imprevisíveis.
O mecanismo: a decisão do modelo sai "solto" (texto livre) e você a colapsa num formato estruturado (JSON Schema), com tipos e enums. Isso restringe a forma da resposta.
O trade-off: structured output garante a sintaxe, não a semântica. O modelo pode devolver um JSON válido com um valor incoerente. Por isso a validação de schema não é opcional, é parte do mecanismo.
Como fazer:
- Declare o JSON Schema esperado (campos, tipos, enums). Ex.: "o pedido do cliente é atendível?
sim|não". - Valide o schema na codebase após receber (uma lib de JSON Schema).
- Trate a falha: se a validação falhar, refaça com guidance mais rígida ou sinalize para revisão. Nunca assuma "veio estruturado, logo está certo".
- Não confie na "verdade universal" do modelo: confie no formato após validação.
Exemplo: um fluxo de classificação devolve {classificacao: "spam"|"nao_spam"}. O modelo erra às vezes, a validação pega valor fora do enum e despacha para revisão ou retry, em vez de classificar errado silenciosamente.
Validação: um teste de regressão que alimenta outputs válidos e inválidos e confirma que o sistema só avança com schema validado.
Princípio 3. Separe memória de contexto de tarefa
O problema: contexto é caro e polui. Mandar história inteira para cada chamada encarece e confunde a resposta.
O mecanismo: dois conceitos distintos. Memória é o histórico persistido (banco), consultada seletivamente. Contexto de tarefa é só o que é relevante para responder aquele prompt específico. A construção de cada prompt escolhe o que pertencente à tarefa.
O trade-off: em chat iterativo, às vezes reconstituir a conversa inteira é necessário para grounding; em análises pontuais, passar só o trecho relevante é melhor (custo, menos ruído). A decisão é por tipo de fluxo.
Como fazer:
- Memória vai para um banco (Mongo ou qualquer store), com busca por relevância.
- Tarefa = apenas os trechos que respondem àquele prompt.
- Desenhe essa separação explícita no pipeline de construção de contexto.
Validação: comparação A/B do mesmo prompt com memória "toda" vs. "relevante", medindo custo, latência e qualidade (ver Princípio 9).
Princípio 4. Proteja o custo com token budget
O problema: o LLM é um centro de custo. Um usuário descontrolado (ou um loop mal escrito) torra seu crédito. Um clique num botão pode disparar uma chamada cara; 99% não abusarão, mas o limite existe para os 1% que sim.
O mecanismo: token budget por usuário, como um rate limit, verificado antes da chamada (store de baixa latência, ex.: Redis).
O trade-off: limite duro (bloqueia) vs. limite suave (degrada/avisa). Para onboarding/upsell, suave; para controle de custo real, duro. E escolha de modelo: request simples → modelo barato; request complexo → modelo caro, roteamento por custo.
Como fazer:
- Token budget por usuário (contagem de tokens, não de requests, prompts variam em tamanho).
- Lógica de leaky bucket, rolling window ou fixed window.
- Roteie por complexidade para custo.
Validação: teste que simula usuário excessivo e confirma o bloqueio/degradação no limite; monitorar % de usuários que batem no teto.
Princípio 5. Faça cache com invalidação
O problema: chamadas de LLM são lentas e caras. Repetir trabalho já feito é jogar dinheiro e tempo fora.
O mecanismo: cache por chave determinística, hash canônico do prompt + versão do modelo + parâmetros. Só responda de cache para chaves exatamente iguais (não "parecidas").
O trade-off (cache vs. recomputação): cache vence quando há alta repetição e o custo de invalidar é baixo. Recomputar vence quando os dados mudam rápido ou a resposta é pessoal/contextual. Cache semântico (prompts "parecidos") é pesquisa ativa, não recomende como prática inicial sem avaliação de risco, pois pode servir conteúdo errado e aparentemente válido.
Como fazer:
- Chave determinística (hash de prompt + modelo + params).
- Defina TTL e versão do prompt/modelo na chave.
- No RAG, cacheie o resultado do retrieval (documentos reusáveis), não só a "palavra-chave".
- Invalidação explícita: TTL + bump de versão. Cache desatualizado é pior que não ter cache.
Validação: teste de invalidação, muda a pergunta cuja resposta dependia de um dado; confirma que o cache não devolve valor velho.
Princípio 6. Mova o trabalho pesado para background
O problema: agregar histórico, categorizar dados ou gerar um perfil demora e custa. Se responde na thread de request, o sistema engasga.
O mecanismo: fila de background jobs. O request síncrono devolve rápido; o fluxo pesado roda em segundo plano e publica o resultado.
O trade-off (síncrono vs. assíncrono): chat/consulta é síncrono por natureza. Análise que agrega estado (histórico, perfil) é assíncrona: evita gargalo de latência e carrega melhor sob concorrência. É o mesmo princípio de upload de vídeo de sempre, LLM não muda a regra.
Como fazer:
- Identifique fluxos que agregam estado do usuário → fila.
- A resposta síncrona devolve rápido; o resultado chega quando fica pronto (poll/webhook/stream).
Validação: teste de carga que mostra tempo p95 síncrono estável mesmo com N jobs pesados enfileirados.
Princípio 7. Modele fluxos como máquinas de estado (e deixe LLMs conversarem)
O problema: LLMs usadas em request/response puro desperdiçam a chance de orquestrar tarefas de várias etapas.
O mecanismo: especifique estados formalmente; cada estado pode chamar uma LLM (ou outra parte do sistema). É uma máquina de estado mesmo que hoje esteja como um monte de if/else. Deixe LLMs conversarem: uma gera (ex.: plano de treino), outra é judge (avalia com o histórico e devolve feedback para regerar).
O trade-off (workflow determinístico vs. agente autônomo): máquina de estado (passos enumeráveis) é determinística e previsível, use quando o caminho é conhecido (formulário, categorização, e-mail). Agente autônomo (agente) é quando o caminho não é enumerável (pesquisa, planos abertos), pague guardrails/judge pelo grau de autonomia. Comece determinístico; só evolua para autônomo quando a tarefa exigir.
Como fazer:
- Liste os estados e transições.
- Cada estado chama a LLM certa e valida a saída (Princípio 2).
- Use judge para loops de melhoria, com critério de parada (N iterações máx).
- Sempre idempotente (Princípio 8).
Exemplo: recebeu e-mail → armazena → categoriza (LLM) → gera resposta (LLM) → aguarda aprovação → envia. GPT gera o plano, Claude julga e devolve feedback.
Validação: teste de fluxo completo com entradas planejadas confirmando cada transição; teste de loop máx iterações.
Princípio 8. Garanta idempotência
O problema: fluxos caros não podem rodar duas vezes para o mesmo input.
O mecanismo: idempotência, se o input é o mesmo, o resultado é o mesmo (ou reutilizado). Ocorre em retries (rede, timeout) e no cliente (usuário bobo clicando duas vezes).
O trade-off: idempotência tem custo (guarda de estado/chave). Em fluxos baratos, um retry simples basta; em fluxos caros (multi-LLM, background), a chave de idempotência é obrigatória. O custo da idempotência escala com o custo do fluxo.
Como fazer:
- Chave determinística do input.
- Cache de resultado ou flag de estado evita reexecução.
- No background, um job não roda duas vezes com a mesma chave.
Validação: teste de retry, envia o mesmo pedido duas vezes e confirma execução única.
Princípio 9. Observe: custo, velocidade, qualidade e groundedness
O problema: você não sabe se o modelo está bom, rápido ou barato até medir. E "bom" não se resume à resposta isolada.
O mecanismo: painel que cruza, por provider/modelo e por input: custo, velocidade, qualidade (um judge, outra LLM, nota a resposta) e groundedness/fidelidade ao contexto. Capacidade ≠ confiabilidade do sistema completo (dunning futura).
O trade-off: eval off-line (gold set + regressão) vs. online (produção). Judge é útil, mas precisa de calibração (comparar com gold set) e não deve ser a única métrica, porque mede resposta isolada, não o fluxo (task success, tool execution success, groundedness).
Como fazer:
- Meça por provider/modelo e por input: custo, latência, qualidade (judge calibrado), groundedness, task success, tool success.
- Monte painel cruzando custo × velocidade × nota; versionamento de modelo/prompt no eixo.
- Roteamento/troca de modelo ao vivo é PROJEÇÃO (possibilidade avançada de produção), não prática de MVP: exige judge calibrado + regressão + versionamento + rollback. Em MVP, priorize painel passivo.
- Métricas que fazem sentido: só as que você consegue coletar e agir, task success rate, tool execution success rate, groundedness, custo e latência por tarefa concluída, taxa de human review.
Validação: um gold set de N casos; judge comparado a ele para medir calibração; teste de regressão antes de qualquer troca de modelo.
MVP vs. produção: o que é obrigatório onde
Para não inflar complexidade, separe:
Obrigatório no MVP
- Provider atrás de port (P1), evita refém.
- Structured output + validação de schema (P2).
- Token budget por usuário com limite duro (P4).
- Error handling razoável: timeout, um retry, idempotência básica.
- Observabilidade mínima: logar custo, latência e falha (P9).
Obrigatório em produção
- Cache com invalidação (P5) e background/async (P6).
- Retries com backoff + idempotência documentada (P8), rate limiting/backpressure real.
- Versionamento de modelo e prompt.
- Eval contínuo: offline + online + regressão + painel de drift (P9).
- Segurança, isolamento e PII.
Regra: adicione cada mecanismo quando houver uma métrica observável que prove sua necessidade, não por antecipação. Complexidade paga com evidência, não com fé.
Trade-offs que sua aplicação vai exigir
- Workflow determinístico vs. agente autônomo: veja P7. Determinístico quando o caminho é conhecido; autônomo quando não é enumerável, pague judge/guardrails pelo grau de autonomia.
- RAG vs. contexto longo: RAG quando os dados são muitos, mutáveis e você precisa groundedness verificável/atualidade; contexto longo quando o corpus é pequeno/estável e a fidelidade importa mais que o custo.
- API gerenciada vs. self-hosted: gerenciada quando time pequeno e custo variável é aceitável; self-hosted quando há requisito de privacidade/custo fixo em escala. Cuidado: self-hosted não reduz LLM engineering, só troca o provider.
- Single-agent vs. multi-agent: comece single. Multi-agent só com papéis genuínos; custa latência, tokens e falhas de orquestração.
- Síncrono vs. assíncrono: veja P6.
- Cache vs. recomputação: veja P5.
- Modelos especialistas vs. generalistas: roteie por tarefa (barato p/ simples, top p/ complexo), só se o tráfego for realmente misto.
Checklist antes do deploy
- Provider atrás de port/adapter (nenhum SDK no core)?
- Saída validada por schema (structured output com validação)?
- Falha de validação tratada (retry/sinalização)?
- Memória separada do contexto de tarefa?
- Token budget por usuário (limite duro se necessário)?
- Cache com invalidação definida e chave determinística?
- Jobs pesados em background?
- Fluxos como máquina de estado?
- Idempotência garantida para retry?
- Painel de custo × velocidade × qualidade (judge calibrado) + groundedness?
- Definido o que é MVP vs. produção (e regra: complexidade paga com métrica)?
- Timeouts, retries com backoff e rate limiting no lugar de produção?
Nove princípios, mas o teste não é decorar o checklist: é observar. Escolha um fluxo real, valide o structured output (P2), meça custo/latência/falha (P9) e verse se roda sem intervenção. Quando um mecanismo some dos logs de falha, você sabe que está cumprindo o papel. São passos verificáveis assim que separam a aplicação "com IA" da aplicação "desenhada para IA".