mozak.tech Engenharia de IA Corporativa 71%

Parte II — Acesso Programático e Instrução de Modelos

2.3 — Instruções Avançadas: Encadeamento, Tentativas e Confiabilidade (Prompt Engineering)

Objetivo da Aula

Ao concluir esta aula, você será capaz de:

Implementar backoff exponencial com jitter para retry de rate limits de API

Construir extração robusta de JSON de respostas de LLM usando estratégias em cascata

Criar chains de prompt que passam contexto estruturado entre etapas sequenciais

Aplicar estratégias de anti-alucinação baseadas em restrições de output e grounding explícito

Debugar falhas em pipelines de prompt identificando onde a cadeia quebrou e por quê

Por que isso importa

Prompts de produção são diferentes de prompts de playground. No playground, você tenta uma vez, vê se funcionou, edita e tenta de novo. Em produção, seu código precisa lidar com rate limits, respostas malformadas, contexto que chega em partes, e LLMs que às vezes inventam dados.

O custo de não fazer isso direito é alto. Uma startup de análise de documentos legais implantou um pipeline RAG sem validação de JSON — quando o Claude retornou JSON com comentários (formato inválido para json.loads()), o pipeline inteiro travou em produção por 2 horas, afetando contratos em revisão. O fix eram 5 linhas de código de extração robusta.

Um segundo problema comum: rate limits. APIs de LLM têm limites de requests por minuto. Em carga alta, você os atinge. Sem retry adequado, cada request que falha é perdido. Com retry ingênuo (tenta de novo imediatamente), você piora o problema — 10 workers tentando simultaneamente quando o rate limit é atingido cria uma tempestade que demora muito mais para se resolver.

E o problema mais insidioso: alucinações. LLMs inventam fatos com total confiança. Em aplicações de suporte ao cliente, análise de documentos, ou qualquer domínio onde precisão importa, alucinações são bugs que chegam ao usuário final como verdades.

Esta aula te dá as ferramentas técnicas para lidar com cada um desses problemas sistematicamente.

Conceitos Fundamentais

Por Que LLMs Falham em JSON

Fundamento: Saída do LLM é Texto Probabilístico

APIs REST tradicionais garantem o formato da resposta — o servidor retorna o schema contratado ou um erro. LLMs não têm essa garantia: a saída é texto gerado probabilisticamente, e o modelo pode adicionar texto antes do JSON, usar aspas simples em vez de duplas, incluir comentários inválidos, ou truncar antes de fechar o objeto. Isso não é bug — é a natureza do modelo. A prática de pedir "retorne JSON" no prompt aumenta a probabilidade de JSON válido, mas não garante. Provedores oferecem mecanismos nativos mais fortes: JSON mode (força estrutura de objeto JSON), structured outputs com schema (força campos e tipos específicos) — use esses quando disponíveis antes de recorrer à extração defensiva por regex.

Pedir a um LLM para retornar JSON parece simples, mas há várias formas de falhar:

Falha 1: Texto antes/depois do JSON

Claro! Aqui está a análise:

{"topicos": ["ML", "IA"], "nivel": "intermediario"}

Espero que ajude!

json.loads() falha porque há texto fora do JSON.

Falha 2: JSON com comentários

{

    "topicos": ["ML"],  // análise técnica

    "nivel": "avancado"

}

JSON não suporta comentários. json.loads() falha.

Falha 3: Aspas simples em vez de duplas

{'topicos': ['ML'], 'nivel': 'avancado'}

JSON requer aspas duplas. É Python dict notation, não JSON válido.

Falha 4: Markdown code block

```json

{"topicos": ["ML"]}

O modelo às vezes envolve JSON em blocos de código markdown.



**Falha 5: Truncamento por max_tokens**

```json

{"topicos": ["Machine Learning", "Redes Ne

O JSON foi cortado no meio porque atingiu o limite de tokens. Resultado inválido.

A solução é uma função de extração em cascata que tenta cada estratégia em ordem:

import json

import re



def extrair_json(texto: str) -> dict:

    # Estratégia 1: JSON direto (caso feliz)

    try:

        return json.loads(texto)

    except json.JSONDecodeError:

        pass



    # Estratégia 2: Remove blocos markdown

    # ``` json\n{...}\n``` ou ```\n{...}\n```

    match = re.search(r'```(?:json)?\s*(\{[\s\S]+?\})\s*```', texto)

    if match:

        try:

            return json.loads(match.group(1))

        except json.JSONDecodeError:

            pass



    # Estratégia 3: Encontra qualquer { ... } ou [ ... ]

    match = re.search(r'(\{[\s\S]+\}|\[[\s\S]+\])', texto)

    if match:

        try:

            return json.loads(match.group(1))

        except json.JSONDecodeError:

            pass



    # Estratégia 4: Tenta reparar aspas simples → duplas

    try:

        reparado = texto.replace("'", '"')

        return json.loads(reparado)

    except json.JSONDecodeError:

        pass



    # Nenhuma estratégia funcionou

    return {'erro': 'JSON inválido', 'texto_original': texto}

Backoff Exponencial: A Matemática

Rate limits são expressos em “X requests por minuto”. Quando você excede, a API retorna HTTP 429 e indica quando você pode tentar de novo (header Retry-After).

A estratégia correta é backoff exponencial com jitter:

Tentativa 1 falha → aguarda 2^0 + jitter = 1.0 + rand(0, 0.5) ≈ 1.3s

Tentativa 2 falha → aguarda 2^1 + jitter = 2.0 + rand(0, 0.5) ≈ 2.3s

Tentativa 3 falha → aguarda 2^2 + jitter = 4.0 + rand(0, 0.5) ≈ 4.2s

Tentativa 4 falha → desiste e propaga erro

O jitter é crítico. Imagine 50 workers que todos atingem o rate limit no mesmo segundo. Sem jitter, todos tentam de novo no mesmo segundo, e o rate limit acontece de novo. Com jitter, as tentativas se espalham no tempo, e a API “respira”.

Cap no delay máximo: Em sistemas de alta disponibilidade, você também coloca um cap (ex: máximo 30s de wait) para não deixar requests presos por minutos:

delay = min(2 ** tentativa + random.uniform(0, 1), 30)  # cap em 30s

Prompt Chaining: O Padrão

Chaining é quebrar uma tarefa complexa em etapas menores onde o output de uma etapa alimenta o input da próxima. É como um pipeline Unix: cat file | grep pattern | sort | uniq.

Por que usar? - Cada passo tem uma tarefa clara → menos erro e alucinação - Você pode inspecionar e validar outputs intermediários - Você pode usar modelos diferentes para diferentes etapas (Haiku para classificação simples, Sonnet para síntese complexa) - Mais fácil de debugar quando falha (sabe exatamente qual etapa)

Estrutura básica:

# Etapa 1: Extração de fatos

resultado_extracao = chamar_llm(prompt_extracao(texto_bruto))

fatos = extrair_json(resultado_extracao)



# Etapa 2: Usa fatos da etapa 1 para classificar

resultado_classificacao = chamar_llm(

    prompt_classificacao(fatos["topicos"], fatos["nivel"])

)

classificacao = extrair_json(resultado_classificacao)



# Etapa 3: Usa resultado de ambas as etapas para gerar saída final

resultado_final = chamar_llm(

    prompt_relatorio(fatos, classificacao)

)

Estratégias de Anti-Alucinação

LLMs alucinam porque são otimizados para gerar texto plausível, não necessariamente verdadeiro. As estratégias efetivas para reduzir alucinação:

1. Grounding explícito (RAG) Forneça a fonte de verdade no prompt e instrua o modelo a só usar essa fonte:

Responda APENAS com base no texto abaixo. Se a informação não estiver no texto, 

diga "não encontrei essa informação no texto fornecido."



TEXTO: {documento}

PERGUNTA: {pergunta}

2. Solicitar citação de fonte

Para cada afirmação factual, cite o parágrafo de onde extraiu a informação.

Se não conseguir citar, não inclua a afirmação.

3. Output estruturado com campo de confiança

{

    "resposta": "...",

    "confianca": "alta|media|baixa",

    "justificativa": "baseado no parágrafo 3 que diz '...'"

}

4. Pedido de cadeia de raciocínio (CoT)

Antes de responder, pense passo a passo:

1. Quais fatos relevantes estão no texto?

2. Quais inferências posso fazer com segurança?

3. O que preciso deduzir (menor confiança)?

Então dê sua resposta final.

5. Consistência via múltiplas chamadas Para decisões críticas, chame o LLM 3x com temperatura alta e use a resposta majoritária (self-consistency):

respostas = [chamar_llm(prompt, temperature=0.7) for _ in range(3)]

# Use votação majoritária ou pede ao modelo para escolher a melhor

Aprofundamento Técnico

O Pattern de Retry Completo

import time

import random

import anthropic



client = anthropic.Anthropic()



def chamar_com_retry(

    mensagens: list[dict],

    max_tokens: int = 500,

    tentativas: int = 3,

    base_delay: float = 1.0,

    max_delay: float = 30.0,

) -> str:

    """

    Backoff exponencial com jitter e cap máximo.

    Diferencia entre erros recuperáveis e não-recuperáveis.

    """

    ultimo_erro = None



    for tentativa in range(tentativas):

        try:

            response = client.messages.create(

                model='claude-haiku-4-5-20251001',

                max_tokens=max_tokens,

                messages=mensagens,

            )

            return response.content[0].text



        except anthropic.RateLimitError as e:

            # Recuperável — rate limit temporário

            ultimo_erro = e

            if tentativa == tentativas - 1:

                raise



            delay = min(base_delay * (2 ** tentativa) + random.uniform(0, 0.5), max_delay)

            print(f"[rate_limit] Tentativa {tentativa+1}/{tentativas}. Aguardando {delay:.1f}s...")

            time.sleep(delay)



        except anthropic.APIConnectionError as e:

            # Recuperável — problema de rede temporário

            ultimo_erro = e

            if tentativa == tentativas - 1:

                raise



            delay = min(base_delay * (2 ** tentativa), max_delay)

            print(f"[connection_error] Tentativa {tentativa+1}/{tentativas}. Aguardando {delay:.1f}s...")

            time.sleep(delay)



        except anthropic.AuthenticationError:

            # NÃO recuperável — chave de API inválida. Não adianta tentar de novo.

            raise



        except anthropic.APIError as e:

            # Outros erros da API — recuperável em alguns casos (5xx), não em outros (4xx)

            if e.status_code and 400 <= e.status_code < 500:

                raise  # erros de cliente (bad request, etc.) — não tentar de novo

            ultimo_erro = e

            if tentativa == tentativas - 1:

                raise

            delay = min(base_delay * (2 ** tentativa), max_delay)

            time.sleep(delay)



    raise RuntimeError(f"Falhou após {tentativas} tentativas: {ultimo_erro}")

Extração Robusta de JSON com Validação de Schema

Uma função de extração realmente robusta não só extrai o JSON, mas valida que tem a estrutura esperada:

import json

import re

from typing import Any, Optional





def extrair_json(texto: str) -> dict:

    """Extrai JSON de texto que pode ter conteúdo ao redor."""

    # Estratégia 1: texto puro é JSON

    try:

        return json.loads(texto.strip())

    except json.JSONDecodeError:

        pass



    # Estratégia 2: JSON em bloco de código markdown

    match = re.search(r'```(?:json)?\s*\n?([\s\S]+?)\n?```', texto)

    if match:

        try:

            return json.loads(match.group(1))

        except json.JSONDecodeError:

            pass



    # Estratégia 3: Primeira ocorrência de { ... } válido

    # Usa greedy para pegar o objeto mais externo completo

    match = re.search(r'\{[\s\S]+\}', texto)

    if match:

        try:

            return json.loads(match.group(0))

        except json.JSONDecodeError:

            pass



    # Estratégia 4: Primeira ocorrência de [ ... ] (para arrays)

    match = re.search(r'\[[\s\S]+\]', texto)

    if match:

        try:

            return json.loads(match.group(0))

        except json.JSONDecodeError:

            pass



    return {'erro': 'JSON inválido após todas as estratégias', 'texto_original': texto[:200]}





def validar_schema(dados: dict, campos_obrigatorios: list[str]) -> tuple[bool, str]:

    """Valida que o JSON extraído tem os campos esperados."""

    campos_faltando = [c for c in campos_obrigatorios if c not in dados]

    if campos_faltando:

        return False, f"Campos ausentes: {campos_faltando}"

    if 'erro' in dados:

        return False, f"Extração falhou: {dados['erro']}"

    return True, "ok"





def chamar_com_json_garantido(

    prompt: str,

    campos_obrigatorios: list[str],

    max_tentativas: int = 3,

) -> dict:

    """

    Chama o LLM até conseguir um JSON válido com os campos esperados.

    Usa o erro da tentativa anterior como feedback no prompt seguinte.

    """

    historico = [{'role': 'user', 'content': prompt}]



    for tentativa in range(max_tentativas):

        resposta_texto = chamar_com_retry(historico, max_tokens=600)

        dados = extrair_json(resposta_texto)

        valido, motivo = validar_schema(dados, campos_obrigatorios)



        if valido:

            return dados



        # Adiciona ao histórico como se fosse conversa multi-turn

        # O modelo vê sua resposta anterior e a instrução de correção

        historico.append({'role': 'assistant', 'content': resposta_texto})

        historico.append({

            'role': 'user',

            'content': (

                f"Sua resposta anterior não era JSON válido ou faltavam campos. "

                f"Motivo: {motivo}. "

                f"Campos obrigatórios: {campos_obrigatorios}. "

                f"Retorne APENAS o JSON, sem texto adicional."

            )

        })



    raise ValueError(f"Não conseguiu JSON válido após {max_tentativas} tentativas")

Prompt Chaining com Contexto Acumulado

O padrão de chain onde cada etapa recebe os resultados de todas as anteriores:

def analisar_artigo_chain(titulo: str, resumo: str) -> dict:

    """

    Chain 2 etapas:

    1. Extrai metadados do artigo

    2. Classifica audiência usando os metadados

    """



    # === ETAPA 1: Análise de conteúdo ===

    prompt_etapa1 = f"""Analise o artigo científico de IA abaixo e retorne JSON.



ARTIGO:

Título: {titulo}

Resumo: {resumo}



Retorne EXATAMENTE este JSON (sem texto adicional):

{{

  "topicos": ["lista de tópicos técnicos"],

  "nivel": "iniciante|intermediario|avancado",

  "tem_codigo": true|false,

  "area_principal": "NLP|CV|RL|teoria|aplicado|outro"

}}"""



    texto_etapa1 = chamar_com_retry([{'role': 'user', 'content': prompt_etapa1}])

    resultado_etapa1 = extrair_json(texto_etapa1)



    # Validação intermediária — falha rápido se etapa 1 quebrou

    campos_etapa1 = ['topicos', 'nivel', 'tem_codigo']

    for campo in campos_etapa1:

        if campo not in resultado_etapa1:

            raise ValueError(f"Etapa 1 falhou: campo '{campo}' ausente. Resposta: {texto_etapa1[:200]}")



    # === ETAPA 2: Classificação de audiência ===

    # Usa os resultados da etapa 1 como contexto estruturado

    prompt_etapa2 = f"""Com base na análise de um artigo científico, classifique a audiência ideal.



ANÁLISE DO ARTIGO:

- Tópicos: {', '.join(resultado_etapa1['topicos'])}

- Nível: {resultado_etapa1['nivel']}

- Tem código: {resultado_etapa1['tem_codigo']}



Retorne EXATAMENTE este JSON:

{{

  "audiencia": "descrição da audiência ideal",

  "prerequisitos": ["conhecimento 1", "conhecimento 2"],

  "tempo_leitura_min": número_inteiro

}}"""



    texto_etapa2 = chamar_com_retry([{'role': 'user', 'content': prompt_etapa2}])

    resultado_etapa2 = extrair_json(texto_etapa2)



    return {

        'titulo': titulo,

        'analise': resultado_etapa1,

        'classificacao': resultado_etapa2,

    }

Por que passar contexto estruturado (JSON) entre etapas, não texto livre?

Compare as duas abordagens para passar dados da etapa 1 para a etapa 2:

# RUIM: passa texto livre da etapa 1

prompt_etapa2 = f"""Com base nesta análise: '{texto_etapa1}', classifique..."""

# O modelo precisa "re-parsear" o texto para entender os dados



# BOM: passa dados estruturados

prompt_etapa2 = f"""Tópicos: {resultado_etapa1['topicos']}

Nível: {resultado_etapa1['nivel']}"""

# O modelo recebe dados precisos, sem ambiguidade

Contexto estruturado reduz a chance de erro de interpretação e torna a cadeia mais confiável.

Exemplos Anotados

Exemplo 1: O Problema do Retry Ingênuo vs Correto

import time

import anthropic



client = anthropic.Anthropic()



# ========================================

# VERSÃO INGÊNUA — não faça assim

# ========================================

def retry_ruim(prompt, tentativas=3):

    for i in range(tentativas):

        try:

            return client.messages.create(

                model='claude-haiku-4-5-20251001',

                max_tokens=200,

                messages=[{'role': 'user', 'content': prompt}]

            ).content[0].text

        except Exception:

            time.sleep(1)  # sleep fixo — piora o thundering herd

    raise RuntimeError("Falhou")



# Problema: 50 workers falham ao mesmo tempo → 50 workers tentam de novo

# 1s depois (juntos) → falham de novo → loop até o rate limit scaler

# resolver o problema por conta própria



# ========================================

# VERSÃO CORRETA — exponential backoff + jitter

# ========================================

import random



def retry_correto(prompt, tentativas=3):

    for tentativa in range(tentativas):

        try:

            return client.messages.create(

                model='claude-haiku-4-5-20251001',

                max_tokens=200,

                messages=[{'role': 'user', 'content': prompt}]

            ).content[0].text

        except anthropic.RateLimitError:

            if tentativa == tentativas - 1:

                raise



            # delay = 2^0=1s, 2^1=2s, 2^2=4s + jitter de 0-0.5s

            # Workers diferentes terão delays ligeiramente diferentes

            delay = (2 ** tentativa) + random.uniform(0, 0.5)

            print(f"Rate limit. Aguardando {delay:.2f}s...")

            time.sleep(delay)

        except anthropic.AuthenticationError:

            raise  # não tentar de novo — chave inválida



# Resultado: requests se espalham no tempo, rate limit se resolve

Exemplo 2: Pipeline Completo com Validação em Cada Etapa

import json

import re

import anthropic



client = anthropic.Anthropic()



def extrair_json(texto: str) -> dict:

    """Extração em cascata — tenta 4 estratégias."""

    # Estratégia 1: JSON direto

    try:

        return json.loads(texto.strip())

    except json.JSONDecodeError:

        pass



    # Estratégia 2: Remove markdown code block

    match = re.search(r'```(?:json)?\s*\n?([\s\S]+?)\n?```', texto)

    if match:

        try:

            return json.loads(match.group(1))

        except json.JSONDecodeError:

            pass



    # Estratégia 3: Encontra { ... }

    match = re.search(r'\{[\s\S]+\}', texto)

    if match:

        try:

            return json.loads(match.group(0))

        except json.JSONDecodeError:

            pass



    # Falhou tudo

    return {'erro': 'JSON inválido', 'raw': texto[:200]}





def analisar_artigo_com_validacao(titulo: str, resumo: str) -> dict:

    """

    Pipeline com:

    1. Retry para falhas de API

    2. Extração robusta de JSON

    3. Validação de campos obrigatórios

    4. Correção automática se etapa falhar

    """



    # ETAPA 1: Extração de metadados

    prompt1 = f"""Analise este artigo e retorne APENAS JSON válido, sem texto adicional.



Título: {titulo}

Resumo: {resumo[:500]}



JSON esperado:

{{

  "topicos": ["topico1", "topico2"],

  "nivel": "iniciante|intermediario|avancado",

  "tem_codigo": true,

  "area_principal": "NLP|CV|RL|teoria|aplicado|outro"

}}"""



    # Retry com backoff

    for tentativa in range(3):

        try:

            resp1 = client.messages.create(

                model='claude-haiku-4-5-20251001',

                max_tokens=300,

                messages=[{'role': 'user', 'content': prompt1}]

            )

            break

        except anthropic.RateLimitError:

            if tentativa == 2:

                raise

            time.sleep(2 ** tentativa + random.uniform(0, 0.5))



    analise = extrair_json(resp1.content[0].text)



    # Validação de campos obrigatórios

    for campo in ['topicos', 'nivel', 'tem_codigo']:

        if campo not in analise or 'erro' in analise:

            raise ValueError(

                f"Etapa 1 produziu JSON inválido. Resposta: {resp1.content[0].text[:200]}"

            )



    # ETAPA 2: Classificação de audiência

    # Usa dados estruturados da etapa 1, não texto bruto

    prompt2 = f"""Classifique a audiência ideal para um artigo de IA.



Dados do artigo:

- Tópicos: {analise['topicos']}

- Nível: {analise['nivel']}

- Tem código de exemplo: {analise['tem_codigo']}

- Área: {analise.get('area_principal', 'não especificado')}



Retorne APENAS JSON:

{{

  "audiencia": "descrição em 1 frase",

  "prerequisitos": ["prerequisito1", "prerequisito2"],

  "tempo_leitura_min": 15

}}"""



    resp2 = client.messages.create(

        model='claude-haiku-4-5-20251001',

        max_tokens=300,

        messages=[{'role': 'user', 'content': prompt2}]

    )



    classificacao = extrair_json(resp2.content[0].text)



    return {

        'titulo': titulo,

        'analise': analise,       # resultado da etapa 1

        'classificacao': classificacao,  # resultado da etapa 2

        'tokens_usados': (

            resp1.usage.input_tokens + resp1.usage.output_tokens +

            resp2.usage.input_tokens + resp2.usage.output_tokens

        )

    }





# Teste

resultado = analisar_artigo_com_validacao(

    titulo='Attention is All You Need',

    resumo='Apresentamos o Transformer, arquitetura baseada exclusivamente em atenção.'

)



print(f"Análise: {resultado['analise']}")

print(f"Classificação: {resultado['classificacao']}")

print(f"Tokens totais do pipeline: {resultado['tokens_usados']}")

Padrões e Armadilhas

Padrões Corretos

Padrão 1: Separar concerns de retry da lógica de negócio

# CORRETO: retry é infraestrutura, fica numa função separada

def com_retry(fn, tentativas=3): ...

def analisar(texto: str) -> dict:

    return com_retry(lambda: chamar_api(texto))



# ERRADO: retry misturado com lógica de negócio — difícil de testar e manter

def analisar(texto: str) -> dict:

    for i in range(3):

        try:

            resultado = chamar_api(texto)

            # ... mais 50 linhas de parsing ...

        except:

            time.sleep(2**i)

Padrão 2: Falhar rápido em erros não-recuperáveis Chave inválida (AuthenticationError), parâmetros inválidos (BadRequestError), input maior que context window — nenhum desses vai melhorar com retry. Detecte e propague imediatamente.

Padrão 3: Multi-turn para correção de JSON Se o JSON extraído está inválido, em vez de só tentar de novo com o mesmo prompt, adicione ao histórico de mensagens a resposta ruim do modelo e uma instrução de correção:

historico = [

    {'role': 'user', 'content': prompt_original},

    {'role': 'assistant', 'content': resposta_invalida},  # o que o modelo retornou

    {'role': 'user', 'content': 'Sua resposta não era JSON válido. Retorne APENAS JSON.'},

]

O modelo vê seu próprio erro e tende a corrigir.

Armadilhas

⚠️ Armadilha 1: Usar except Exception para tudo no retry

# ERRADO: captura erros não-recuperáveis e tenta de novo desnecessariamente

except Exception:

    time.sleep(2**tentativa)



# CORRETO: diferencia por tipo de erro

except anthropic.RateLimitError:

    # recuperável

except anthropic.AuthenticationError:

    raise  # não recuperável — imediatamente

⚠️ Armadilha 2: max_tokens muito baixo trunca o JSON

# PERIGOSO: se o JSON gerado for maior que 200 tokens, vai truncar

max_tokens=200  # para prompt que pede JSON com 5 campos



# Calcule: cada campo do JSON ≈ 10-20 tokens

# JSON com 5 campos complexos ≈ 100-300 tokens

# Dê margem: max_tokens >= 2x o tamanho esperado do JSON

max_tokens=600

⚠️ Armadilha 3: Passar texto livre (não estruturado) entre etapas de chain

# ERRADO: o texto da etapa 1 vai para o prompt da etapa 2 como texto bruto

prompt_etapa2 = f"Com base nesta análise: '{texto_resposta_etapa1}', classifique..."

# O modelo precisa "re-entender" o texto, o que pode gerar erros



# CORRETO: parseia JSON da etapa 1 e passa campos específicos

analise = extrair_json(texto_resposta_etapa1)

prompt_etapa2 = f"Tópicos: {analise['topicos']}\nNível: {analise['nivel']}"

⚠️ Armadilha 4: Não validar outputs intermediários do chain

# ERRADO: passa o resultado da etapa 1 para a etapa 2 sem checar

resultado_etapa1 = extrair_json(resposta1)

prompt_etapa2 = f"Topicos: {resultado_etapa1['topicos']}"  # KeyError se campo ausente!



# CORRETO: valida antes de usar

if 'topicos' not in resultado_etapa1:

    raise ValueError(f"Etapa 1 falhou — campo 'topicos' ausente")

⚠️ Armadilha 5: Temperatura alta em prompts que pedem JSON

# PERIGOSO: temperature alta → mais criatividade → mais chance de malformar JSON

temperature=0.9  # para prompt de extração de dados



# CORRETO: temperatura baixa para outputs estruturados

temperature=0.0  # máximo determinismo para JSON

temperature=0.1  # ou muito baixa — permite alguma variação mas mantém estrutura
⚗ Laboratório prático — mozak.tech
Se não for realizar o laboratório, pule para o próximo capítulo.

Ponte para o Lab

O starter tem 3 TODOs que cobrem os conceitos centrais desta aula:

TODO 1 — Em chamar_com_retry(), a estrutura de retry já existe mas falta o backoff exponencial. O código atual tem time.sleep(2**tentativa) como comentário. A versão completa deve adicionar jitter: espera = 2**tentativa + random.uniform(0, 0.5) e time.sleep(espera). O if tentativa == tentativas - 1: raise já está correto — na última tentativa, propaga o erro. Seção de referência: Conceitos Fundamentais → “Backoff Exponencial”.

TODO 2 — Em extrair_json(), implemente extração em cascata. A função atual só tenta json.loads() diretamente. Adicione: primeiro tente direto, depois regex para markdown code block (```json ... ```), depois regex para { ... } qualquer. Se tudo falhar, retorne {'erro': 'JSON inválido', 'texto_original': texto[:100]}. Seção de referência: Conceitos Fundamentais → “Por Que LLMs Falham em JSON” e Exemplos Anotados → Exemplo 2.

TODO 3 — Em analisar_artigo_chain(), a etapa 2 já tem o placeholder. Construa um segundo prompt que usa resultado_analise['topicos'] e resultado_analise['nivel'] como input estruturado. O prompt deve pedir JSON com campos audiencia, prerequisitos, e tempo_leitura_min. Chame chamar_com_retry() com esse prompt e retorne o JSON extraído. Seção de referência: Aprofundamento Técnico → “Prompt Chaining com Contexto Acumulado”.

Após implementar os três TODOs, o pipeline vai: chamar a API com retry automático → extrair JSON robustamente → passar dados estruturados entre etapas. O main() do starter testa com dois artigos reais e imprime os resultados de cada etapa.

Agora você está pronto para o lab.