Capítulo 4: Fundamentos de Prompting e Raciocínio

Nos capítulos anteriores, você dominou os fundamentos técnicos de LLMs: arquitetura Transformer (Capítulo 1), processo de treinamento (Capítulo 2) e técnicas de adaptação via fine-tuning (Capítulo 3). Com esse conhecimento sobre a construção interna dos modelos, agora é essencial dominar a interface primária de interação com eles: os prompts.

Prompting não é simplesmente “escrever instruções” para um modelo de linguagem. É uma disciplina técnica fundamentada em princípios de design de interface, teoria da informação e psicologia cognitiva aplicada a sistemas de IA. Um prompt bem construído pode representar a diferença entre um sistema que opera com 40% de acurácia e outro que alcança 95% na mesma tarefa, usando o mesmo modelo subjacente.

Sim, eu também tinha preconceito com “prompting” até estudar isso de fato e entender sua profundidade técnica.

Para engenheiros construindo agentes autônomos, dominar prompting é tão fundamental quanto dominar APIs para desenvolvimento de software tradicional. Agentes de IA são, em essência, sistemas que orquestram prompts complexos através de ciclos de percepção-raciocínio-ação. Sem compreensão profunda de como estruturar instruções, fornecer contexto e guiar raciocínio, é impossível construir agentes confiáveis e robustos.

Este capítulo estabelece os fundamentos de prompting através de quatro pilares:

  1. Princípios de Estruturação: Anatomia de prompts efetivos e padrões de organização
  2. In-Context Learning: Técnicas zero-shot, few-shot e many-shot para adaptação sem fine-tuning
  3. Chain-of-Thought Reasoning: Métodos para raciocínio complexo multi-etapa
  4. ReAct Framework: Fundamentos de reasoning + acting para agentes autônomos

Cada seção combina teoria fundamentada com implementações práticas, progressivamente construindo o conhecimento necessário para sistemas de agentes em produção.

Anatomia e Estruturação de Prompts

Componentes de um Bom Prompt

Um prompt eficiente possui uma estrutura clara que separa responsabilidades e facilita a interpretação pelo modelo. A arquitetura típica consiste em cinco componentes principais:

# Estrutura canônica de um prompt
prompt = f"""
<ROLE>
Você é um especialista em análise de dados financeiros
</ROLE>

<TASK>
Analise o seguinte relatório trimestral e identifique 3 principais riscos financeiros
</TASK>

<CONTEXT>
Empresa: PoneyCorp
Setor: Software B2B
Receita Trimestre: $2.5M (-15% YoY)
Burn Rate: $800K/mês
Runway: 8 meses
</CONTEXT>

<FORMAT>
Para cada risco, forneça:
1. Descrição (máximo 50 palavras)
2. Severidade (Baixa/Média/Alta/Crítica)
3. Recomendação de mitigação (máximo 100 palavras)
</FORMAT>

<CONSTRAINTS>
Foque apenas em dados quantitativos apresentados, sem fazer suposições sobre causas externas. Priorize riscos de liquidez sobre riscos estratégicos na análise.
</CONSTRAINTS>
"""

Justificativa técnica para cada componente:

O componente ROLE ativa padrões específicos aprendidos durante pré-treinamento. Modelos expostos a milhões de documentos técnicos desenvolvem representações internas de como especialistas de domínio se comunicam. Especificar role alinha a distribuição de saída com esses padrões, aumentando a qualidade técnica da resposta.

O componente TASK define o objetivo de otimização para geração. Sem task explícita, o modelo pode gerar continuações plausíveis mas irrelevantes, já que predição de próximo token não garante utilidade prática. Uma task bem definida ancora o modelo no objetivo desejado.

O CONTEXT fornece informação não disponível nos pesos do modelo. Foundation models possuem conhecimento geral sobre o mundo, mas não têm acesso a dados específicos da sua aplicação, como métricas financeiras atuais ou situações particulares que você precisa analisar.

O FORMAT especifica o schema de resposta, sendo crucial para parsing programático e integração em pipelines. Formatos estruturados reduzem variabilidade em aproximadamente 80% (Anthropic s.d.), facilitando o processamento automatizado das respostas.

Por fim, CONSTRAINTS define limites operacionais. Modelos tendem a extrapolar além do contexto fornecido, fenômeno conhecido como alucinação. Constraints explícitas reduzem significativamente esse comportamento, mantendo o modelo focado nos dados disponíveis.

Técnicas de Marcação Estrutural

A escolha do formato de marcação impacta diretamente a interpretabilidade pelo modelo. Três abordagens são dominantes para diferentes casos de uso: Markdown, XML e JSON.

Markdown

A linguagem de marcação Markdown é amplamente utilizada devido à sua simplicidade e legibilidade humana. É ideal para prompts destinados a usuários finais ou quando a clareza visual é prioritária.

# Análise de Sentimento

## Tarefa
Classifique o sentimento do texto como POSITIVO, NEGATIVO ou NEUTRO.

## Exemplos
**Texto**: "Produto excelente, recomendo!"  
**Sentimento**: POSITIVO

**Texto**: "Qualidade terrível, não funciona."  
**Sentimento**: NEGATIVO

## Input
**Texto**: "Produto ok, nada de especial."  
**Sentimento**:

Vantagens: Alta legibilidade humana, bem representado em dados de treinamento (documentação técnica, READMEs).

Desvantagens: Parsing pode ser ambíguo para estruturas muito complexas.

XML (Recomendado para Estrutura Hierárquica)

A linguagem XML oferece uma estrutura hierárquica clara, ideal para prompts que exigem múltiplos níveis de aninhamento ou extração de dados estruturados.

<prompt>
  <task>Extrair informações estruturadas de texto legal</task>
  <document>
    <type>contrato</type>
    <content>
    O CONTRATANTE, empresa XYZ LTDA, CNPJ 12.345.678/0001-90,
    contrata os serviços de consultoria pelo período de 12 meses...
    </content>
  </document>
  <extraction_schema>
    <field name="contratante" type="string"/>
    <field name="cnpj" type="string" pattern="XX.XXX.XXX/XXXX-XX"/>
    <field name="duracao_meses" type="integer"/>
  </extraction_schema>
</prompt>

XML oferece hierarquia clara e parsing não-ambíguo, com suporte nativo otimizado no Claude (Anthropic s.d.). Como desvantagem principal, a verbosidade aumenta a contagem de tokens entre 15-30% comparado a Markdown equivalente, impactando custos em casos de alto volume.

JSON (Recomendado para Integração Programática)

JSON é ideal para prompts que exigem integração direta com sistemas de software, APIs ou quando a saída precisa ser parseada programaticamente.

{
  "role": "data_analyst",
  "task": "time_series_forecasting",
  "data": {
    "historical": [120, 135, 148, 152, 160],
    "frequency": "monthly",
    "unit": "thousands_usd"
  },
  "parameters": {
    "horizon": 3,
    "confidence_interval": 0.95
  },
  "output_format": {
    "predictions": "array[float]",
    "confidence_bounds": "object{lower: array, upper: array}"
  }
}

JSON permite parsing trivial, integração direta com código e validação via JSON Schema. No entanto, apresenta legibilidade reduzida para prompts complexos, modelos podem gerar JSON mal-formado exigindo validação adicional, e a sintaxe estrita com vírgulas e aspas é propensa a erros de geração.

Escolha de Formato: Matriz de Decisão

A escolha do formato de marcação deve considerar múltiplos fatores técnicos e operacionais:

Critério Markdown XML JSON
Legibilidade Humana Excelente Boa Regular
Parsing Programático Regular Excelente Excelente
Hierarquia Complexa Regular Excelente Muito Boa
Eficiência de Tokens Excelente Boa Muito Boa
Robustez (Geração) Muito Boa Muito Boa Regular
Suporte Nativo LLM Excelente Muito Boa (Claude) Boa
NotaMetodologia de Avaliação

As avaliações desta tabela baseiam-se em:

  1. Legibilidade Humana: Análise qualitativa de facilidade de leitura/edição
  2. Parsing Programático: Complexidade de implementação (regex vs. parsers nativos)
  3. Hierarquia Complexa: Capacidade de representar estruturas aninhadas (3+ níveis)
  4. Eficiência de Tokens: Medições empíricas em prompts equivalentes (seção seguinte)
  5. Robustez (Geração): Taxa de erros sintáticos observados em outputs de GPT-4/Claude (dados internos, não publicados)
  6. Suporte Nativo LLM: Documentação oficial de APIs (XML explicitamente otimizado no Claude (Anthropic s.d.))

Limitação: Não há benchmarks públicos sistematizando essas dimensões. Avaliações refletem práticas observadas em produção, mas podem variar por caso de uso específico.

Heurísticas de seleção:

Markdown deve ser usado quando o prompt será lido ou editado por humanos frequentemente, especialmente em estruturas simples com apenas 1-2 níveis de hierarquia. É ideal para integração com documentação existente como READMEs e wikis, situações onde minimização de tokens é prioritária, e quando o formato de saída esperado é texto livre ou semi-estruturado. Exemplos típicos incluem prompts para usuários finais, documentação técnica e instruções de sistema.

XML é recomendado para hierarquias profundas e complexas com 3 ou mais níveis, extração estruturada de múltiplos campos aninhados, e especialmente quando Claude é o modelo primário devido ao suporte nativo otimizado. Use também quando houver necessidade de atributos e metadados através de XML attributes, ou quando parsing não-ambíguo for crítico. Casos comuns incluem extração de entidades de documentos legais e parsing de relatórios técnicos complexos.

JSON deve ser escolhido quando a saída será consumida diretamente por APIs ou código, validação via schema for necessária (usando JSON Schema), houver integração com sistemas existentes como REST APIs, tipos de dados precisarem ser preservados (números, booleanos, arrays), ou quando o pipeline de processamento requerer formato padronizado. Exemplos típicos são agentes que interagem com APIs, sistemas de extração de dados e integração com databases.

Comparação Prática: Mesmo Prompt em Três Formatos

Para ilustrar as diferenças, considere a mesma tarefa de extração de informações implementada nos três formatos:

Markdown:

# Tarefa: Extração de Dados de Produto

Extraia as seguintes informações do texto: Nome completo do produto, Preço em BRL (apenas número), Desconto percentual (se houver), e Avaliação de 1 a 5 estrelas.

## Texto de Entrada:
"Smartphone XPhone Pro - R$ 2.499,00 (20% OFF) ⭐⭐⭐⭐⭐"

## Saída:

XML:

<task>
  <description>Extração de Dados de Produto</description>
  <fields>
    <field name="nome" type="string" description="Nome completo do produto"/>
    <field name="preco" type="float" description="Valor em BRL"/>
    <field name="desconto" type="int" optional="true" description="Percentual"/>
    <field name="avaliacao" type="int" min="1" max="5" description="Estrelas"/>
  </fields>
  <input>
    Smartphone XPhone Pro - R$ 2.499,00 (20% OFF) ⭐⭐⭐⭐⭐
  </input>
  <output_format>
    <produto>
      <nome>...</nome>
      <preco>...</preco>
      <desconto>...</desconto>
      <avaliacao>...</avaliacao>
    </produto>
  </output_format>
</task>

JSON:

{
  "task": "product_extraction",
  "schema": {
    "nome": {"type": "string", "required": true},
    "preco": {"type": "number", "required": true, "unit": "BRL"},
    "desconto": {"type": "integer", "required": false, "range": [0, 100]},
    "avaliacao": {"type": "integer", "required": true, "range": [1, 5]}
  },
  "input": "Smartphone XPhone Pro - R$ 2.499,00 (20% OFF) ⭐⭐⭐⭐⭐",
  "output_example": {
    "nome": "Smartphone XPhone Pro",
    "preco": 2499.00,
    "desconto": 20,
    "avaliacao": 5
  }
}

Análise comparativa:

Aspecto Markdown XML JSON
Tokens ~120 ~180 (+50%) ~150 (+25%)
Tempo para escrever 2-3 min 4-5 min 3-4 min
Parsing (Python) Regex complexo xml.etree json.loads()
Manutenção Simples Média Simples
Validação Manual XML Schema JSON Schema
DicaRecomendação Geral

Para a maioria dos casos de uso em agentes de IA:

  1. Comece com Markdown (simplicidade, tokens, legibilidade)
  2. Migre para JSON se precisar de parsing robusto ou integração com APIs
  3. Use XML apenas para hierarquias muito complexas ou quando usar Claude

Evite otimização prematura, Markdown resolve a maioria dos casos eficientemente.

Sistema de Templates e Variáveis

Para sistemas em produção, prompts raramente são estáticos. Normalmente misturamos componentes fixos (instruções, formato) com dados dinâmicos que podem ser entradas de usuários, dados de outros sistemas, ou algum contexto adicional que recuperamos de algum lugar.

Templates parametrizados permitem reutilização e manutenção de um mesmo prompt para múltiplos casos de uso, simplesmente substituindo variáveis conforme necessário.

Exemplo:

from string import Template

# Template base para classificação
CLASSIFICATION_TEMPLATE = Template("""
Classifique o seguinte $entity_type segundo as categorias: $categories.

$entity_type: "$entity"

Critérios de Classificação:
$criteria

Categoria:
""")

# Uso
prompt = CLASSIFICATION_TEMPLATE.substitute(
    entity_type="documento legal",
    entity="Contrato de prestação de serviços...",
    categories="CONTRATO, PROCURAÇÃO, ALVARÁ, CERTIDÃO",
    criteria="""
    CONTRATO refere-se a acordos bilaterais com obrigações entre partes. PROCURAÇÃO indica delegação de poderes de uma parte a outra. ALVARÁ representa autorização governamental para atividades específicas. CERTIDÃO é documento comprobatório emitido por órgão oficial.
    """
)

Vantagens de templates:

Templates oferecem manutenibilidade aprimorada porque alterações em um único lugar propagam automaticamente para todos os usos. Eles melhoram a testabilidade ao permitir que variações de prompts sejam testadas sistematicamente. Templates também podem ser versionados como código, facilitando controle de mudanças e rollback quando necessário. Por fim, proporcionam alta reutilização, onde o mesmo template pode servir múltiplos domínios através de parametrização simples.

Templating em Produção

Em sistemas de produção reais, normalmente escolhemos alguma ferramenta que nos ajude a gerenciar templates de forma eficiente. LangChain PromptTemplate oferece framework especializado para LLMs com validação integrada. F-strings Python trazem simplicidade para casos básicos sem dependências. Mustache e Handlebars são agnósticos a linguagem de programação, facilitando consistência em stacks poliglota. Jinja2 é amplamente usado em aplicações web e já está presente em stacks Flask ou Ansible.

Vamos ver exemplos práticos de cada abordagem.

Exemplos de Templating

LangChain PromptTemplate:

# uv pip install langchain-core

from langchain_core.prompts import PromptTemplate

# Template simples
template = PromptTemplate.from_template("""
Analise o seguinte {doc_type}:

{content}

Forneça análise focando em: {focus_areas}
""")

prompt = template.format(
    doc_type="relatório financeiro",
    content="Receita Q3: $2.5M...",
    focus_areas="liquidez, crescimento, eficiência operacional"
)

Alternativa: f-strings para casos simples:

def build_analysis_prompt(doc_type: str, content: str, focus_areas: list[str], examples: list[dict] | None = None) -> str:
    """
    Constrói prompt de análise com exemplos opcionais.
    
    Args:
        doc_type: Tipo de documento
        content: Conteúdo a analisar
        focus_areas: Áreas de foco
        examples: Exemplos opcionais de análises anteriores
    """
    prompt_parts = [f"Analise o seguinte {doc_type}:\n\n{content}\n"]
    
    if examples:
        prompt_parts.append("\nExemplos de análises anteriores:")
        for i, ex in enumerate(examples, 1):
            prompt_parts.append(f"{i}. {ex['summary']}")
            prompt_parts.append(f"   Conclusão: {ex['conclusion']}\n")
    
    focus_str = ", ".join(focus_areas)
    prompt_parts.append(f"\nForneça análise focando em: {focus_str}")
    
    return "\n".join(prompt_parts)

# Uso
prompt = build_analysis_prompt(
    doc_type="relatório financeiro",
    content="Receita Q3: $2.5M...",
    focus_areas=["liquidez", "crescimento", "eficiência operacional"],
    examples=[
        {"summary": "Q2 análise", "conclusion": "Crescimento sustentável"},
        {"summary": "Q1 análise", "conclusion": "Burn rate elevado"}
    ]
)

In-Context Learning: Adaptação sem Atualização de Pesos

In-Context Learning (ICL) representa uma das capacidades emergentes mais significativas de Large Language Models (Brown et al. 2020). Conforme discutido no Capítulo 2, ICL permite que modelos adaptem seu comportamento para novas tarefas através de exemplos fornecidos no prompt, sem necessidade de gradient descent ou atualização de parâmetros.

Esta seção explora as três modalidades principais de ICL: Zero-Shot, Few-Shot e Many-Shot Learning. Veremos suas fundamentações teóricas, limitações práticas e heurísticas para seleção de abordagem.

Zero-Shot Learning: Transferência de Conhecimento Implícito

Zero-shot learning explora o conhecimento adquirido durante pré-treinamento para realizar tarefas sem exemplos explícitos. O modelo mapeia a instrução para padrões vistos em dados de treinamento.

Formulação:

Dado um modelo \(M\) com parâmetros \(\theta\) fixos e uma tarefa \(T\) descrita por instrução \(I\), zero-shot learning busca:

\[ \hat{y} = \arg\max_{y} P(y | I, x; \theta) \]

onde \(x\) é o input e \(y\) a saída desejada, sem exemplos \((x_i, y_i)\) da tarefa \(T\).

Em termos práticos: O modelo recebe apenas uma descrição textual da tarefa (a instrução \(I\)) e o input específico (\(x\)), sem ver nenhum exemplo de como a tarefa deve ser executada. Ele deve “adivinhar” a saída correta baseando-se exclusivamente no que aprendeu durante o pré-treinamento. É como pedir a alguém para executar uma tarefa nova explicando apenas verbalmente o que fazer, sem mostrar nenhum exemplo concreto.

Por exemplo, ao pedir “Classifique o sentimento deste texto: ‘Adorei o produto!’”, o modelo:

  1. Interpreta a instrução “Classifique o sentimento”
  2. Mapeia para padrões similares vistos em treinamento (reviews, análises de sentimento em textos)
  3. Gera a resposta mais provável: “Positivo”

Faça um teste no Ollama, execute o prompt acima e veja como cada modelo responde!

Vamos ver uma implementação simples de zero-shot classification para ilustrar:

def zero_shot_classify(text: str, categories: list[str]) -> str:
    """
    Classificação zero-shot usando instrução explícita.
    
    Args:
        text: Texto a classificar
        categories: Lista de categorias possíveis
    
    Returns:
        Categoria prevista
    """
    prompt = f"""
Classifique o seguinte texto em uma das categorias: {', '.join(categories)}

Texto: "{text}"

Categoria:"""
    
    response = llm.generate(prompt, temperature=0.0)
    return response.strip()

# Exemplo
categorias = ["SPAM", "URGENTE", "INFORMATIVO", "MARKETING"]
texto = "Última chance! Desconto de 70% apenas hoje!"
resultado = zero_shot_classify(texto, categorias)
# Output esperado: "MARKETING"

Quando zero-shot é suficiente:

  1. Tarefas bem estabelecidas: Tradução, sumarização, Q&A factual
  2. Vocabulário comum: Tarefas descritas em termos familiares ao modelo
  3. Baixa ambiguidade: Critérios de decisão objetivos e universais
  4. Restrições de latência: Minimizar tokens de input reduz tempo de inferência

Limitações conhecidas:

  1. Inconsistência de formato: Sem exemplos, formato de saída pode variar
  2. Interpretação ambígua: Instruções podem ser mal interpretadas
  3. Conhecimento de cauda longa: Tarefas de domínio específico falham frequentemente
NotaTrade-off Latência vs. Acurácia

Zero-shot minimiza tokens de input (tipicamente 50-200 tokens vs. 500-2000 em few-shot), resultando em latência 2-5× mais rápida, custo 3-10× menor, mas acurácia frequentemente 10-30% inferior em tarefas complexas.

Decisão deve basear-se em trade-off explícito entre performance e custo operacional.

Few-Shot Learning: Demonstração por Exemplos

Few-shot learning fornece exemplos demonstrativos (\(k\) exemplos, tipicamente \(2 \leq k \leq 10\)) que especificam formato, estilo e critérios de decisão.

Formulação:

Dado conjunto de exemplos \(\mathcal{E} = \{(x_1, y_1), ..., (x_k, y_k)\}\) da tarefa \(T\):

\[ \hat{y} = \arg\max_{y} P(y | \mathcal{E}, x; \theta) \]

O modelo aprende o mapeamento \(x \rightarrow y\) através de pattern matching nos exemplos.

Em termos práticos: Few-shot learning funciona mostrando ao modelo alguns exemplos do que você quer antes de fazer a pergunta real. É como ensinar alguém a preencher um formulário mostrando 2-3 exemplos já preenchidos. O modelo reconhece o padrão nos exemplos (input → output, formato, estilo de resposta) e aplica esse mesmo padrão ao novo caso.

Por exemplo, para classificar sentimento: “Produto excelente!” → POSITIVO, “Péssima qualidade.” → NEGATIVO, “Produto ok.” → NEUTRO. Com estes três exemplos, o modelo infere que “Adorei a qualidade!” deve ser classificado como POSITIVO.

O modelo vê que você quer classificações em MAIÚSCULAS, com uma palavra só, baseado no tom do texto. Ele aplica esse padrão e responde: “POSITIVO”.

Seleção dinâmica de exemplos:

Em vez de usar exemplos fixos, podemos selecionar dinamicamente os exemplos mais relevantes usando similaridade semântica:

class FewShotSelector:
    def select_examples(self, query: str) -> list[dict]:
        """Seleciona k exemplos mais similares ao query."""
        query_embedding = self.encoder.encode([query])
        similarities = cosine_similarity(query_embedding, self.embeddings)[0]
        top_k_indices = np.argsort(similarities)[-self.k:][::-1]
        return [self.example_bank[i] for i in top_k_indices]
NotaImplementação Completa

Para implementação completa do seletor few-shot com embeddings e pré-computação, veja o Exemplo 1: Few-Shot Selector com Embeddings.

Resultados empíricos

Em estudos comparativos (Brown et al. 2020), few-shot learning consistentemente supera zero-shot em tarefas complexas. Foram usados os benchmarks SuperGLUE, TriviaQA e PIQA e os ganhos observados foram:

Tarefa Zero-Shot Few-Shot (k=10) Ganho
SuperGLUE 51.3% 71.8% +20.5%
TriviaQA 64.3% 71.2% +6.9%
PIQA 70.2% 82.3% +12.1%

Many-Shot Learning: Explorando Context Windows Longos

Many-shot learning (\(k > 10\), frequentemente \(50 \leq k \leq 500\)) explora context windows de modelos de contexto longo (100k+ tokens) para aproximar performance de fine-tuning.

Fundamentação teórica:

Com \(k\) suficientemente grande, ICL pode aproximar fine-tuning via gradient descent implícito no espaço de atenção (Akyürek et al. 2022).

Formulação:

\[ \lim_{k \to \infty} P_{\text{ICL}}(y | \mathcal{E}_k, x) \approx P_{\text{FT}}(y | x; \theta') \]

Onde \(\theta'\) são parâmetros após fine-tuning em \(\mathcal{E}\).

Em termos práticos: Many-shot learning envolve fornecer ao modelo uma grande quantidade de exemplos diretamente no prompt, aproveitando a enorme capacidade de context window dos modelos modernos. Isso permite que o modelo “aprenda” o comportamento desejado observando muitos exemplos, sem a necessidade de ajustar seus pesos.

Gerenciamento de context window:

Para many-shot learning, é crucial gerenciar o context window do modelo para incluir o máximo de exemplos possível sem ultrapassar o limite:

class ManyShotPromptBuilder:
    def build_prompt(self, examples, query, task_description, tokenizer):
        """Preenche context window com máximo de exemplos possível."""
        available_tokens = self.max_tokens - overhead
        
        for ex in examples:
            if current_tokens + ex_tokens > available_tokens:
                break
            selected_examples.append(ex)
        
        return prompt, len(selected_examples)
NotaImplementação Completa

Para implementação completa com gerenciamento inteligente de tokens e context window, veja o Exemplo 2: Many-Shot Prompt Builder.

Trade-offs de many-shot:

Aspecto Many-Shot Few-Shot Fine-Tuning
Acurácia Alta (85-95%) Média (70-85%) Muito Alta (90-98%)
Custo por chamada Alto ($0.50-$5.00) Baixo ($0.01-$0.10) Muito Baixo ($0.005-$0.02)
Latência Alta (5-15s) Baixa (0.5-2s) Muito Baixa (0.2-1s)
Adaptabilidade Imediata Imediata Requer retreino
Manutenção Simples Simples Complexa
AvisoQuando Many-Shot NÃO Compensa

Many-shot é economicamente viável apenas quando:

  1. Volumetria baixa: < 1000 chamadas/dia (custo de fine-tuning não amortiza)
  2. Dados em evolução: Exemplos mudam frequentemente (fine-tuning seria retreinado constantemente)
  3. Múltiplas tarefas: Mesmo context window serve tarefas diferentes

Para aplicações de alto volume (> 10k chamadas/dia), fine-tuning apresenta ROI superior em 2-4 semanas.

Seleção de Estratégia ICL: Heurísticas Práticas

A decisão entre zero-shot, few-shot, many-shot ou até mesmo fine-tuning não é binária. Ela depende de múltiplos fatores técnicos e de negócio que devem ser avaliados em conjunto.

Matriz de decisão por critério:

Critério Zero-Shot Few-Shot Many-Shot
Volume de Chamadas Alto (>10k/dia) Médio (1k-10k/dia) Baixo (<1k/dia)
Acurácia Necessária Baixa-Média (60-80%) Média-Alta (70-90%) Muito Alta (85-95%)
Latência Tolerável Crítica (<500ms) Moderada (500ms-2s) Alta (2s-15s)
Custo por Chamada Muito Baixo (<$0.01) Baixo ($0.01-$0.10) Alto ($0.50-$5.00)
Frequência de Mudança de Dados Alta (diária/semanal) Moderada (mensal) Baixa (semestral/anual)
DicaInterpretando a Tabela

Exemplo prático: Se você tem uma aplicação com 5k chamadas/dia, precisa de 85% de acurácia, tolera até 3s de latência e pode gastar até $0.50/chamada, a tabela sugere Few-Shot (volume médio) ou Many-Shot (acurácia alta + budget OK + latência tolerável).

Para decisões limítrofes entre múltiplas estratégias, use a árvore de decisão abaixo para desempate.

Árvore de decisão para seleção:

A árvore abaixo prioriza os fatores na ordem: volume → acurácia → custo/latência, com fine-tuning sendo recomendado sempre que o volume justifica o investimento inicial.

graph TD
    A[Tarefa Nova] --> B{Volume > 10k/dia?}
    B -->|Sim| C[Fine-Tuning]
    B -->|Não| D{Acurácia > 90%?}
    D -->|Sim| E{Budget > $1/chamada?}
    E -->|Sim| F[Many-Shot]
    E -->|Não| C
    D -->|Não| G{Latência < 500ms?}
    G -->|Sim| H[Zero-Shot]
    G -->|Não| I[Few-Shot]
    
    style C fill:#ffcccc
    style F fill:#ffffcc
    style H fill:#ccffcc
    style I fill:#ccf

Como usar a árvore:

  1. Comece sempre avaliando volume: Se você tem >10k chamadas/dia, fine-tuning provavelmente será mais econômico em 2-4 semanas de operação (ROI positivo)

  2. Se volume for baixo/médio, avalie acurácia: Acurácia >90% geralmente requer many-shot ou fine-tuning, enquanto few-shot e zero-shot ficam tipicamente entre 60-85%.

  3. Considere trade-offs de custo vs. latência: Zero-shot é mais rápido e barato mas menos preciso. Few-shot equilibra custo, latência e acurácia. Many-shot é mais caro e lento, mas aproxima performance de fine-tuning.

  4. Valide com dados reais: Esta árvore é heurística. Sempre teste com seu dataset e métricas específicas antes de escalar para produção.

AvisoLimitação da Heurística

Esta árvore assume dados de qualidade razoável (limpos, representativos), task bem definida (clear input-output mapping), e modelo moderno (GPT-4, Claude 3+, Gemini Pro).

Para tarefas ambíguas, dados ruidosos ou modelos menores, os limites de acurácia podem ser diferentes.

Chain-of-Thought (CoT) Prompting

No Capítulo 2, você aprendeu sobre CoT como uma capacidade emergente: a habilidade de modelos grandes resolverem problemas complexos através de raciocínio explícito passo-a-passo. Agora você dominará como aplicar CoT na prática para maximizar a qualidade de raciocínio em seus agentes.

Fundamentos do Raciocínio Passo-a-Passo

CoT funciona criando “espaço de trabalho” no contexto onde o modelo pode raciocinar antes de gerar a resposta final. Em vez de pular direto para a conclusão, o modelo explicita o raciocínio intermediário.

Comparação: Raciocínio Direto vs. Chain-of-Thought

# Raciocínio Direto (frequentemente falha em problemas complexos)
prompt = """
Roger tem 5 bolas de tênis. Ele compra 2 latas de 3 bolas cada.
Quantas bolas tem agora?
"""
# Modelo pequeno: "8" (ERRADO - tentou adivinhar sem raciocinar)

# Chain-of-Thought (força raciocínio explícito)
prompt = """
Roger tem 5 bolas de tênis. Ele compra 2 latas de 3 bolas cada.
Quantas bolas tem agora?

Vamos pensar passo a passo:
"""
# Modelo grande:
# "1. Roger começa com 5 bolas
#  2. Ele compra 2 latas
#  3. Cada lata tem 3 bolas, então 2 × 3 = 6 bolas compradas
#  4. Total: 5 + 6 = 11 bolas
#  Resposta: 11"

Por que CoT funciona?

Relembrando do Capítulo 2, duas razões principais:

  1. Espaço de Trabalho: Tokens intermediários permitem que o modelo “pense” em vez de comprimir todo o raciocínio em uma única predição
  2. Padrão Aprendido: Dados de treinamento contêm muitos exemplos de raciocínio explícito (tutoriais, provas matemáticas), CoT ativa esse padrão

Zero-Shot CoT: “Let’s Think Step by Step”

A descoberta mais surpreendente sobre CoT: simplesmente adicionar “Let’s think step by step” (ou “Vamos pensar passo a passo”) melhora drasticamente a performance, sem fornecer exemplos.

Implementação básica:

def zero_shot_cot(problema):
    prompt = f"""
{problema}

Let's think step by step.
"""
    return llm.generate(prompt)

# Exemplo
problema = "Se um trem viaja a 80 km/h por 2.5 horas, quantos quilômetros percorre?"
resposta = zero_shot_cot(problema)
# "Let's think step by step:
#  1. Velocidade = 80 km/h
#  2. Tempo = 2.5 horas
#  3. Distância = Velocidade × Tempo
#  4. Distância = 80 × 2.5 = 200 km
#  Resposta: 200 quilômetros"

Resultados empíricos (do paper original):

No benchmark GSM8K (problemas matemáticos), a acurácia saltou de 17% para 52% (+35 pontos percentuais). SVAMP (problemas de palavras aritméticas) melhorou de 64% para 92% (+28 pontos), enquanto AQuA (álgebra) passou de ~30% para ~70% (+40 pontos).

Variantes de Prompts Zero-Shot CoT

A frase mágica “Let’s think step by step” tem várias variantes eficazes que podem funcionar melhor dependendo do domínio e tipo de problema:

1. Variante Original (uso geral)

prompt = f"{problema}\n\nLet's think step by step."
# ou em português: "Vamos pensar passo a passo."

Quando usar: Padrão para problemas matemáticos, lógicos e de raciocínio geral.

2. Variante Estruturada (maior controle)

prompt = f"{problema}\n\nLet's solve this problem step by step, showing all work:"
# ou: "Vamos resolver este problema passo a passo, mostrando todo o trabalho:"

Quando usar: Quando você precisa de raciocínio explícito detalhado (ex: debugging, provas matemáticas).

3. Variante com Verificação (maior acurácia)

prompt = f"{problema}\n\nLet's think step by step, then double-check our answer:"
# ou: "Vamos pensar passo a passo e depois verificar nossa resposta:"

Quando usar: Problemas críticos onde erro é caro (cálculos financeiros, dosagens médicas).

4. Variante Específica de Domínio (melhor contexto)

# Para código
prompt = f"{codigo}\n\nLet's analyze this code step by step, checking for bugs:"

# Para dados
prompt = f"{dados}\n\nLet's analyze this data step by step, looking for patterns:"

# Para lógica
prompt = f"{argumento}\n\nLet's evaluate this argument step by step, checking validity:"

Quando usar: Quando o domínio específico tem jargão ou padrões de raciocínio particulares.

Quando zero-shot CoT funciona melhor:

Zero-shot CoT é particularmente eficaz para problemas matemáticos e lógicos, raciocínio de senso comum, análise de código, e problemas que requerem múltiplos passos claros de dedução.

Few-Shot CoT: Ensinando com Exemplos de Raciocínio

Few-shot CoT fornece exemplos de raciocínio completo antes do problema real. O modelo aprende não apenas o formato da resposta, mas como “raciocinar”.

Estrutura de few-shot CoT:

prompt = f"""
Q: Tom tem 3 maçãs. Ele compra mais 2. Quantas maçãs tem agora?
A: Vamos resolver passo a passo:
1. Tom começa com 3 maçãs
2. Ele compra 2 maçãs a mais
3. Total = 3 + 2 = 5 maçãs
Resposta: 5 maçãs

Q: Sarah tem 10 balas. Ela come 3 e dá 2 para o irmão. Quantas sobram?
A: Vamos resolver passo a passo:
1. Sarah começa com 10 balas
2. Ela come 3, sobram 10 - 3 = 7 balas
3. Ela dá 2 ao irmão, sobram 7 - 2 = 5 balas
Resposta: 5 balas

Q: {problema}
A: Vamos resolver passo a passo:
"""

Diferença Critical: Few-Shot ICL vs. Few-Shot CoT

A distinção entre Few-Shot ICL (In-Context Learning) e Few-Shot CoT (Chain-of-Thought) é fundamental para escolher a abordagem correta em seus agentes. Ambos fornecem exemplos, mas o que você mostra nos exemplos determina o comportamento do modelo.

Few-Shot ICL (apenas input-output):

"""
Q: 2 + 3 = ?
A: 5

Q: 7 - 4 = ?
A: 3

Q: 6 × 2 = ?
A:
"""
# O modelo aprende o PADRÃO (formato de pergunta → resposta),
# mas NÃO aprende o PROCESSO de raciocínio.

Few-Shot CoT (mostra o raciocínio):

"""
Q: 2 + 3 = ?
A: Somando 2 + 3, obtemos 5. Resposta: 5

Q: 7 - 4 = ?
A: Subtraindo 4 de 7, obtemos 3. Resposta: 3

Q: 6 × 2 = ?
A:
"""
# O modelo aprende COMO RACIOCINAR explicitamente,
# não apenas qual formato seguir.

Resultado empírico:

Em problemas aritméticos de palavras (GSM8K), Few-Shot ICL alcança ~35% de acurácia, enquanto Few-Shot CoT atinge ~65% (+30 pontos percentuais). O custo adicional de CoT (2-3× mais tokens) frequentemente compensa pela redução de erros e necessidade de retrabalho.

Quando few-shot CoT supera zero-shot:

Few-shot CoT é superior em problemas com estrutura de raciocínio específica do domínio, quando você quer controlar o estilo do raciocínio, em tarefas onde zero-shot CoT ainda falha, e quando os exemplos podem demonstrar edge cases importantes.

Quando usar cada abordagem:

Aspecto Few-Shot ICL Few-Shot CoT
Tipo de tarefa Classificação, extração, formato Raciocínio multi-etapa, matemática, análise
Complexidade Simples, direta Complexa, requer decomposição
Tokens usados Baixo (~50-100/exemplo) Alto (~100-300/exemplo)
Acurácia em problemas simples Alta (80-90%) Similar, mas mais caro
Acurácia em problemas complexos Baixa-Média (50-70%) Alta (75-90%)
Custo Menor Maior (2-3× mais tokens)

Self-Consistency: Múltiplos Caminhos para uma Resposta

Self-consistency gera múltiplas cadeias de raciocínio (com sampling de temperatura > 0) e usa voting majoritário para escolher a resposta final.

A ideia por trás desta abordagem é que se o mesmo resultado aparece por caminhos de raciocínio diferentes, provavelmente está correto.

Conceito básico:

def self_consistency_cot(problema, n_samples=5):
    """Gera múltiplas cadeias e retorna resposta por voting."""
    respostas = []
    
    for i in range(n_samples):
        output = llm.generate(prompt_base, temperature=0.7)
        resposta = extrair_numero_final(output)
        respostas.append(resposta)
    
    # Voting majoritário
    contagem = Counter(respostas)
    resposta_final, votos = contagem.most_common(1)[0]
    return resposta_final
NotaImplementação Completa

Para implementação completa com extração robusta de respostas e métricas de confiança, veja o Exemplo 3: Self-Consistency com Voting.

Se você não entende o que o Counter faz, aqui está uma explicação rápida: é uma classe do módulo collections que conta a frequência de elementos em um iterável. No nosso caso, estamos usando para contar quantas vezes cada resposta apareceu nas cadeias de raciocínio.

respostas = [5, 5, 100, 5, 5]

# Passo 1: Counter cria dicionário de frequências
contagem = Counter(respostas)
# contagem = Counter({5: 4, 100: 1})

# Passo 2: most_common(1) retorna lista com tupla do mais comum
mais_comum = contagem.most_common(1)
# mais_comum = [(5, 4)]

# Passo 3: [0] pega a primeira tupla
tupla = mais_comum[0]
# tupla = (5, 4)

# Passo 4: Desempacota
resposta_final, votos = tupla
# resposta_final = 5
# votos = 4

Por que self-consistency funciona:

  1. Diversidade de caminhos: temperature=0.7 faz o modelo gerar raciocínios diferentes
  2. Erros não-sistemáticos: Erros aleatórios aparecem em apenas algumas cadeias
  3. Voting filtra outliers: Resposta correta tende a aparecer mais vezes
  4. Confiança mensurável: Proporção de votos indica certeza (4/5 = 80%)

Nos estudos originais, self-consistency mostrou ganhos significativos: GSM8K melhorou de 52% (CoT simples) para 58% (+6 pontos), SVAMP de 92% para 95% (+3 pontos), e problemas complexos apresentaram ganhos ainda maiores (+10-20 pontos). Porém, o custo e latência aumentam proporcionalmente ao número de amostras.

Resumo rápido de self-consistency:

Self-consistency melhora acurácia significativamente através de voting que filtra erros aleatórios, mas tem custo 5-10× maior (múltiplas gerações) e latência 5-10× mais lenta (gerações sequenciais ou paralelas).

Quando usar self-consistency:

Use self-consistency em problemas críticos onde acurácia é mais importante que custo, decisões de alto impacto (financeiras, médicas, legais), quando você pode fazer inferências em paralelo, e em problemas onde o modelo erra inconsistentemente.

Técnicas Avançadas de Reasoning

Além de Chain-of-Thought e Self-Consistency, existem técnicas especializadas de raciocínio que se tornaram importantes para construir agentes robustos. Esta seção explora três padrões: ReAct (fusão de raciocínio com ações externas), Self-Reflection (autocorreção e refinamento iterativo) e Analogical Prompting (raciocínio por transferência de analogias).

ReAct: Reasoning + Acting

ReAct (Yao et al. 2023) representa uma mudança em como agentes de IA interagem com o mundo. Em vez de raciocínio isolado ou execução de ações sem contexto, ReAct intercala pensamento e ação, permitindo que agentes sejam adaptativos e responsivos.

O ciclo ReAct segue quatro passos principais:

  1. Raciocinem sobre qual ação tomar (Thought)
  2. Executem ações externas (Action) — chamar APIs, buscar databases, usar ferramentas
  3. Observem os resultados (Observation)
  4. Ajustem o raciocínio baseado nas observações

ReAct segue o seguinte formato estruturado:

Question: [pergunta do usuário]

Thought 1: [raciocínio sobre próximo passo]
Action 1: [ferramenta a usar + argumentos]
Observation 1: [resultado da ação]

Thought 2: [raciocínio baseado na observação]
Action 2: [próxima ação]
Observation 2: [resultado]

...

Thought N: [conclusão final]
Answer: [resposta para o usuário]

Comparando três abordagens, CoT puro, ReAct e Act puro, em problemas que requerem informações externas, pesquisadores chegaram às seguintes conclusões:

Abordagem Descrição Problema
CoT puro Raciocina até o fim, depois age Não pode ajustar plano baseado em resultados
Act puro Executa ações sem raciocinar Ações aleatórias, sem estratégia
ReAct Intercala pensamento e ação Adapta-se dinamicamente a observações

Vamos ver um exemplo comparativo para ilustrar a diferença.

Pergunta: “Quanto custaria comprar 3 unidades do produto mais barato da categoria ‘eletrônicos’?”

CoT puro (falha):

Thought: Preciso encontrar o produto mais barato em eletrônicos
         e multiplicar o preço por 3.
Action: [tenta acessar database sem ter preço específico]
# FALHA: Não sabe qual produto buscar sem primeiro consultar

ReAct (sucesso):

Thought 1: Primeiro preciso listar produtos da categoria 'eletrônicos'
Action 1: search_products(category="eletrônicos")
Observation 1: [Mouse: R$50, Teclado: R$80, Headset: R$120]

Thought 2: O mais barato é Mouse a R$50. Agora multiplico por 3.
Action 2: calculate(50 * 3)
Observation 2: 150

Thought 3: Tenho a resposta final.
Answer: Custaria R$150 para comprar 3 unidades do produto mais barato
        (Mouse a R$50 cada).

Perceba como o agente ReAct ajusta seu raciocínio com base nas observações, permitindo uma solução mais eficaz.

Estrutura básica de um agente ReAct:

class ReActAgent:
    def __init__(self, tools: Dict[str, Callable]):
        """Registry de ferramentas disponíveis."""
        self.tools = tools
        self.history = []

    def run(self, question: str, max_iterations: int = 7) -> str:
        """Loop Thought-Action-Observation até resposta final."""
        for i in range(max_iterations):
            response = llm.generate(prompt, temperature=0.0)
            thought, action, action_input = self._parse_response(response)
            
            if self._is_final_answer(thought):
                return self._extract_answer(response)
            
            observation = self.tools[action](action_input)
            self.history.append(f"Thought/Action/Observation...")
            prompt = self._update_prompt(question, self.history)
        
        return "Limite de iterações atingido."
NotaImplementação Completa

Para implementação completa do agente ReAct com parsing robusto, tratamento de erros e exemplos de ferramentas, veja o Exemplo 4: ReAct Agent Completo.

Ferramentas de exemplo:

def calculator(expression: str) -> float:
    """Avalia expressões matemáticas simples."""
    return eval(expression)  # ATENÇÃO: inseguro em produção!

def search_database(query: str) -> List[Dict]:
    """Busca produtos no database."""
    # Simulação - em produção seria chamada real
    return mock_db.get(categoria, [])

Quando usar ReAct:

Use ReAct quando precisa de informações externas (APIs, databases, web search), quando ferramentas devem ser escolhidas dinamicamente (não é sequência fixa de ações), quando o resultado de uma ação influencia a próxima (decisões condicionais), e quando debugging é importante (histórico Thought-Action-Observation facilita diagnóstico).

Limitações conhecidas:

  1. Loops infinitos: Agente pode repetir mesmas ações (mitigação: limite de iterações, detecção de loops)
  2. Parsing frágil: LLM pode não seguir formato exato (mitigação: retry com correção do formato)
  3. Custo elevado: Múltiplas chamadas ao LLM (mitigação: caching, modelos menores para ações simples)

Self-Reflection: Autocorreção e Refinamento Iterativo

Self-reflection permite que agentes avaliem suas próprias respostas e as refinem iterativamente. Em vez de gerar uma resposta e parar, o agente:

  1. Gera resposta inicial
  2. Critica a própria resposta (identifica erros, lacunas)
  3. Refina baseado na crítica
  4. Repete até satisfazer critérios ou limite de iterações

A estrutura típica de self-reflection é:

Initial Answer: [primeira tentativa]
Reflection: [autocrítica - o que está errado? o que falta?]
Refined Answer: [versão melhorada]
Reflection: [nova crítica]
Final Answer: [versão final após N iterações]

Vamos analisar um exemplo prático.

Pergunta: “Explique o teorema de Pitágoras para uma criança de 10 anos.”

Sem self-reflection:

Answer: O teorema de Pitágoras afirma que a² + b² = c², onde c é a
        hipotenusa de um triângulo retângulo.
# Resposta tecnicamente correta, mas inadequada para criança de 10 anos

Com self-reflection:

Initial Answer: O teorema de Pitágoras afirma que a² + b² = c², onde c
                é a hipotenusa de um triângulo retângulo.

Reflection: Esta explicação usa termos técnicos (hipotenusa, exponenciação)
            que uma criança de 10 anos provavelmente não conhece. Falta
            contexto visual e exemplo concreto. Preciso simplificar.

Refined Answer: Imagine um triângulo que tem um canto reto (como um
                canto de mesa). Se você souber o tamanho dos dois lados
                que formam esse canto reto, consegue descobrir o tamanho
                do terceiro lado usando uma fórmula mágica de matemática!

Reflection: Melhor, mas ainda abstrato. Deveria dar exemplo numérico simples.

Final Answer: Imagine um triângulo com um canto reto (como canto de mesa).
              Se um lado mede 3cm e o outro mede 4cm, o lado diagonal vai
              medir 5cm. Isso sempre funciona seguindo a regra:
              (3×3) + (4×4) = (5×5) → 9 + 16 = 25. Legal, né?

Loop de refinamento iterativo:

def self_reflection_loop(question, max_iterations=3, quality_threshold=0.8):
    """Loop de self-reflection com refinamento iterativo."""
    answer = llm.generate(f"Question: {question}\nAnswer:", temperature=0.7)

    for i in range(max_iterations):
        # Autocrítica
        reflection = llm.generate(reflection_prompt, temperature=0.3)
        score = extract_quality_score(reflection)
        
        if score >= quality_threshold:
            return answer
        
        # Refinamento
        answer = llm.generate(refinement_prompt, temperature=0.7)
    
    return answer
NotaImplementação Completa

Para implementação completa com prompts de reflexão estruturados e extração de scores, veja o Exemplo 5: Self-Reflection Loop.

Quando self-reflection funciona melhor:

  1. Tarefas criativas: Escrita, design de prompts, geração de código
  2. Respostas complexas: Onde primeira tentativa raramente é ideal
  3. Critérios subjetivos: Qualidade de explicação, adequação ao público-alvo
  4. Tempo não é crítico: Múltiplas gerações aumentam latência significativamente

Variante: External Feedback Loop

Em vez de autocrítica (modelo avalia a si mesmo), use feedback externo:

def reflection_with_external_feedback(question: str, answer: str) -> str:
    """Usa ferramenta externa para avaliar resposta."""

    # Exemplo: verificador de código
    if is_code_question(question):
        test_results = run_unit_tests(answer)

        feedback_prompt = f"""
Your code:
{answer}

Test results:
{test_results}

The tests show your code has bugs. Fix them:

Corrected Code:"""

        return llm.generate(feedback_prompt)

    # Exemplo: fact-checker para respostas factuais
    if is_factual_question(question):
        fact_check_results = verify_facts(answer)
        # ... refinar baseado em fact-checking

Analogical Prompting: Raciocínio por Transferência

Analogical prompting (Yasunaga, Leskovec, e Liang 2023) explora a capacidade de LLMs de gerar suas próprias analogias para resolver problemas novos, baseando-se em problemas similares conhecidos.

O conceito central é que, em vez de memorizar exemplos fixos, o modelo pode recordar problemas similares que já sabe resolver e transferir o raciocínio para o novo problema.

Em vez de fornecer exemplos fixos (few-shot), peça ao modelo para:

  1. Recordar problemas similares que ele já sabe resolver
  2. Gerar soluções para esses problemas análogos
  3. Transferir o raciocínio para o problema atual

A estrutura típica é:

Problem: [problema novo]

Recall relevant problems you know how to solve that are analogous to this one.

Analogous Problem 1: [modelo gera problema similar]
Solution: [modelo gera solução]

Analogous Problem 2: [outro problema similar]
Solution: [solução]

Now solve the original problem using insights from the analogies:
Solution: [solução final]

Vamos ver um exemplo concreto.

Problema: “Um tanque enche em 6 horas com a torneira A e em 3 horas com a torneira B. Em quanto tempo enche com as duas abertas?”

Prompt tradicional (few-shot CoT):

[Fornece 2-3 exemplos similares resolvidos]
Now solve: [problema do tanque]

Analogical prompting:

Problem: Um tanque enche em 6 horas com a torneira A e em 3 horas com
         a torneira B. Em quanto tempo enche com as duas abertas?

Recall relevant problems you know how to solve:

Analogous Problem 1: Dois trabalhadores pintam uma casa. Um leva 4 horas
                     sozinho, outro leva 6 horas. Quanto tempo levam juntos?
Solution: Taxa combinada = 1/4 + 1/6 = 5/12 por hora → 12/5 = 2.4 horas

Analogous Problem 2: Dois canos enchem uma piscina. Um enche em 8h, outro
                     em 12h. Quanto tempo juntos?
Solution: Taxa combinada = 1/8 + 1/12 = 5/24 por hora → 24/5 = 4.8 horas

Now solve the original problem:
Taxa A = 1/6 tanques por hora
Taxa B = 1/3 tanques por hora
Taxa combinada = 1/6 + 1/3 = 1/6 + 2/6 = 3/6 = 1/2 por hora
Tempo = 1 / (1/2) = 2 horas

Answer: 2 horas

Por que funciona?

  1. Ativa conhecimento latente: Modelo “lembra” padrões de solução similares em seus pesos
  2. Generalização: Em vez de memorizar exemplos fixos, modelo identifica estrutura abstrata do problema
  3. Menos exemplos necessários: Modelo gera as próprias analogias

No paper original (Yasunaga, Leskovec, e Liang 2023), analogical prompting superou few-shot CoT: GSM8K melhorou de 87% (few-shot CoT) para 92% (+5 pontos), Math de 23% para 29% (+6 pontos), usando apenas 2-3 analogias geradas versus 8 exemplos few-shot fixos.

Vamos implementar uma função simples de analogical prompting.

def analogical_prompting(problem: str, n_analogies: int = 2) -> str:
    """
    Resolve problema gerando analogias automaticamente.

    Args:
        problem: Problema a resolver
        n_analogies: Número de analogias a gerar

    Returns:
        Solução usando raciocínio analógico
    """
    prompt = f"""
Problem: {problem}

Before solving, recall {n_analogies} problems you know how to solve that are
analogous to this one. For each analogy, show the problem and solution.

Analogous Problem 1:"""

    # Geração de analogias + solução
    response = llm.generate(prompt, temperature=0.7)

    # Adiciona pedido para solução final
    final_prompt = f"""
{response}

Now solve the original problem using insights from these analogies.

Solution to original problem:"""

    solution = llm.generate(final_prompt, temperature=0.3)

    return solution

Quando usar analogical prompting:

  1. Domínios com padrões claros: Matemática, física, programação
  2. Quando você não tem exemplos prontos: Modelo gera as próprias analogias
  3. Problemas estruturalmente similares a conhecidos: Transferência funciona melhor
  4. Reduzir custo de few-shot: Menos tokens que fornecer 5-10 exemplos completos

Limitações:

  1. Analogias podem ser inadequadas: Modelo pode gerar problemas não-análogos
  2. Menos controle: Você não escolhe os exemplos (modelo escolhe)
  3. Não funciona para domínios muito específicos: Se modelo não tem conhecimento prévio similar

Combinando Técnicas

Técnicas de reasoning não são mutuamente exclusivas. Normalmente você pode combiná-las para maximizar desempenho.

Algumas combinações comuns são:

ReAct + Self-Reflection:

Thought: [raciocínio]
Action: [ferramenta]
Observation: [resultado]
Reflection: [essa ação foi útil? preciso mudar estratégia?]
Thought: [raciocínio ajustado]
...

Analogical + Self-Consistency:

Analogical prompting combinado com self-consistency gera 5 soluções usando analogias diferentes, aplica voting majoritário para resposta final, e combina criatividade de analogias com robustez de self-consistency.

CoT + ReAct + Reflection:

Esta combinação usa ReAct para orquestração de ações, CoT para raciocínio em cada Thought, e Reflection para avaliar se a sequência de ações faz sentido. Estas técnicas serão fundamentais quando construirmos agentes autônomos multi-ferramenta nos próximos capítulos.

Pratique o que Aprendeu

Este capítulo apresentou fundamentos de prompting e raciocínio, incluindo estruturação de prompts, in-context learning, chain-of-thought e técnicas avançadas como ReAct e self-reflection.

Para consolidar seu entendimento através de implementação prática, consulte os Exercícios Práticos do Capítulo 4, que incluem:

  1. Few-Shot Dynamic Classifier: Classificador com seleção dinâmica de exemplos por similaridade
  2. Chain-of-Thought Math Solver: Comparação entre zero-shot CoT, few-shot CoT e self-consistency
  3. Simple ReAct Agent: Agente básico com ferramentas (calculadora, APIs, conversores)

Os exercícios fornecem código executável e guias passo a passo para aplicação prática dos conceitos apresentados.

Conclusão

Neste capítulo, você dominou os fundamentos técnicos de prompting e raciocínio que são essenciais para construir sistemas de agentes de IA robustos e eficazes em produção.

O que você aprendeu:

1. Estruturação de Prompts: Anatomia de prompts efetivos usando ROLE, TASK, CONTEXT, FORMAT e CONSTRAINTS. Técnicas de marcação através de Markdown, XML e JSON, incluindo quando usar cada formato. Templates com Jinja2 para produção escalável.

2. In-Context Learning (ICL): Zero-shot (\(P(y | I, x; \theta)\)) incluindo quando usar e suas limitações. Few-shot (\(P(y | \mathcal{E}, x; \theta)\)) com seleção dinâmica de exemplos via embeddings. Many-shot como aproximação de gradient descent com gerenciamento de context window. Trade-offs entre custo, latência e acurácia para cada estratégia.

3. Chain-of-Thought (CoT): Zero-shot CoT usando “Let’s think step by step” para raciocínio emergente. Few-shot CoT com exemplos demonstrando raciocínio explícito. Self-consistency através de voting majoritário para maior robustez (+5-15% acurácia, 5× custo). Quando CoT funciona (raciocínio matemático, planejamento) e quando falha.

4. ReAct Framework: Padrão Pensamento-Ação-Observação para agentes autônomos. Integração de ferramentas externas (calculadora, APIs, databases). Fundamentos para sistemas multi-agentes (Parte II do livro).

Implementações práticas: Exercício 1 demonstra classificador few-shot com seleção dinâmica (+20% acurácia). Exercício 2 implementa math solver com CoT e self-consistency (86% acurácia). Exercício 3 constrói agente ReAct básico com três ferramentas.

Próximos passos:

Estas técnicas formam a base conceitual para todos os sistemas de agentes que construiremos nas próximas partes do livro.

Capítulo 5 (Dominando LLMs na Prática) explora configurações avançadas (temperature, top-p), otimização de context window, e estratégias de custo/latência para produção. Parte II (Construindo Sistemas de Agentes) cobre RAG, tool calling, orquestração e comunicação multi-agente. Parte III (Arquitetura e Produção) aborda verificação, guardrails, segurança, deployment e avaliação.

Prompting é a linguagem de comunicação com LLMs. Dominar essa linguagem não é opcional para quem constrói agentes de IA - é a diferença entre sistemas que funcionam ocasionalmente e sistemas que operam com confiabilidade em produção.

Exemplos Completos de Código

Esta seção apresenta implementações completas e executáveis dos conceitos principais discutidos no capítulo. Cada exemplo é standalone e pode ser executado diretamente.

Exemplo 1: Few-Shot Selector com Embeddings

Implementação completa de seletor dinâmico de exemplos baseado em similaridade semântica para in-context learning.

# uv pip install sentence-transformers scikit-learn numpy

from sentence_transformers import SentenceTransformer
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
from typing import List, Dict

class FewShotSelector:
    """
    Seletor dinâmico de exemplos few-shot baseado em similaridade semântica.
    """
    
    def __init__(self, example_bank: List[Dict], k: int = 5):
        """
        Args:
            example_bank: Lista de dicts com 'input' e 'output'
            k: Número de exemplos a selecionar
        """
        self.example_bank = example_bank
        self.k = k
        self.encoder = SentenceTransformer('all-MiniLM-L6-v2')
        
        # Pré-computar embeddings
        inputs = [ex['input'] for ex in example_bank]
        self.embeddings = self.encoder.encode(inputs)
    
    def select_examples(self, query: str) -> List[Dict]:
        """
        Seleciona k exemplos mais similares ao query.
        
        Args:
            query: Input atual para classificar
        
        Returns:
            Lista de k exemplos mais relevantes
        """
        query_embedding = self.encoder.encode([query])
        similarities = cosine_similarity(query_embedding, self.embeddings)[0]
        
        # Top-k índices
        top_k_indices = np.argsort(similarities)[-self.k:][::-1]
        
        return [self.example_bank[i] for i in top_k_indices]
    
    def build_prompt(self, query: str, task_description: str) -> str:
        """
        Constrói prompt few-shot com exemplos selecionados.
        """
        examples = self.select_examples(query)
        
        prompt_parts = [task_description, ""]
        
        for ex in examples:
            prompt_parts.append(f"Input: {ex['input']}")
            prompt_parts.append(f"Output: {ex['output']}")
            prompt_parts.append("")
        
        prompt_parts.append(f"Input: {query}")
        prompt_parts.append("Output:")
        
        return "\n".join(prompt_parts)

# Exemplo de uso
bank = [
    {"input": "Produto excelente!", "output": "POSITIVO"},
    {"input": "Péssima qualidade.", "output": "NEGATIVO"},
    {"input": "Produto ok.", "output": "NEUTRO"},
    {"input": "Melhor compra que fiz!", "output": "POSITIVO"},
    {"input": "Não recomendo.", "output": "NEGATIVO"},
    {"input": "Funciona bem mas poderia ser melhor.", "output": "NEUTRO"},
    {"input": "Adorei! Superou expectativas!", "output": "POSITIVO"},
    {"input": "Horrível, total desperdício.", "output": "NEGATIVO"}
]

selector = FewShotSelector(bank, k=3)
prompt = selector.build_prompt(
    query="Adorei a qualidade!",
    task_description="Classifique o sentimento como POSITIVO, NEGATIVO ou NEUTRO:"
)

print("Prompt gerado:")
print(prompt)

Resultados esperados:

Características principais:

Este exemplo demonstra seleção automática dos 3 exemplos mais similares semanticamente, oferece performance superior à seleção aleatória (~20% de melhoria em acurácia), e utiliza embeddings pré-computados para eficiência.

Exemplo 2: Many-Shot Prompt Builder

Sistema de construção de prompts many-shot com gerenciamento inteligente de context window.

# uv pip install tiktoken

import tiktoken
from typing import List, Dict, Tuple

class ManyShotPromptBuilder:
    """
    Construtor de prompts many-shot com gerenciamento de context window.
    """
    
    def __init__(self, max_tokens: int = 100_000):
        """
        Args:
            max_tokens: Limite máximo de tokens (ex: 128k para Claude 3.5)
        """
        self.max_tokens = max_tokens
    
    def build_prompt(
        self,
        examples: List[Dict],
        query: str,
        task_description: str,
        tokenizer
    ) -> Tuple[str, int]:
        """
        Constrói prompt many-shot respeitando limite de tokens.
        
        Args:
            examples: Lista completa de exemplos disponíveis
            query: Input atual
            task_description: Descrição da tarefa
            tokenizer: Tokenizer para contagem de tokens
        
        Returns:
            (prompt_final, n_exemplos_incluídos)
        """
        # Tokens reservados para task description e query
        overhead = len(tokenizer.encode(task_description + query)) + 100
        available_tokens = self.max_tokens - overhead
        
        # Adicionar exemplos até preencher context window
        selected_examples = []
        current_tokens = 0
        
        for ex in examples:
            ex_text = f"Input: {ex['input']}\nOutput: {ex['output']}\n\n"
            ex_tokens = len(tokenizer.encode(ex_text))
            
            if current_tokens + ex_tokens > available_tokens:
                break
            
            selected_examples.append(ex_text)
            current_tokens += ex_tokens
        
        # Construir prompt final
        prompt = task_description + "\n\n"
        prompt += "".join(selected_examples)
        prompt += f"Input: {query}\nOutput:"
        
        return prompt, len(selected_examples)

# Exemplo de uso
builder = ManyShotPromptBuilder(max_tokens=128_000)

# Simular banco grande de exemplos
large_example_bank = [
    {"input": f"Exemplo {i}", "output": f"Resultado {i}"}
    for i in range(500)
]

tokenizer = tiktoken.get_encoding("cl100k_base")

prompt, n_examples = builder.build_prompt(
    examples=large_example_bank,
    query="Novo input para classificar",
    task_description="Classifique documentos legais nas categorias...",
    tokenizer=tokenizer
)

print(f"Prompt construído com {n_examples} exemplos")
print(f"Total de tokens: {len(tokenizer.encode(prompt))}")

Características:

O sistema preenche automaticamente o context window com o máximo de exemplos possível, respeita limite de tokens do modelo, e é útil para aproximar performance de fine-tuning sem treinar.

Exemplo 3: Self-Consistency com Voting

Implementação de self-consistency para melhorar robustez através de múltiplas cadeias de raciocínio.

# uv pip install collections

from collections import Counter
import re
from typing import Tuple, List

def self_consistency_cot(problema: str, n_samples: int = 5) -> Tuple[int, float]:
    """
    Gera múltiplas cadeias de raciocínio e retorna resposta por voting.
    
    Args:
        problema: Problema a resolver
        n_samples: Número de cadeias independentes a gerar
    
    Returns:
        (resposta_final, confiança)
    """
    prompt_base = f"{problema}\n\nLet's think step by step."
    
    respostas = []
    raciocinios = []
    
    for i in range(n_samples):
        print(f"\n{'='*60}")
        print(f"Cadeia {i+1}/{n_samples}")
        print(f"{'='*60}")
        
        # Gera cadeia de raciocínio com temperatura > 0 para diversidade
        output = llm.generate(prompt_base, temperature=0.7)
        
        print(output)
        
        # Extrai apenas a resposta final (número, categoria, etc.)
        resposta = extrair_numero_final(output)
        
        if resposta is not None:
            respostas.append(resposta)
            raciocinios.append(output)
            print(f"\nResposta extraída: {resposta}")
        else:
            print("\n⚠️ Falha ao extrair resposta desta cadeia")
    
    if not respostas:
        return None, 0.0
    
    # Voting majoritário
    contagem = Counter(respostas)
    resposta_final, votos = contagem.most_common(1)[0]
    confianca = votos / len(respostas)
    
    print(f"\n{'='*60}")
    print("RESULTADO FINAL")
    print(f"{'='*60}")
    print(f"Distribuição de votos: {dict(contagem)}")
    print(f"Resposta escolhida: {resposta_final}")
    print(f"Confiança: {confianca:.1%} ({votos}/{len(respostas)} votos)")
    
    return resposta_final, confianca

def extrair_numero_final(texto: str) -> int:
    """Extrai número da resposta final."""
    # Procura por "Resposta: X" ou "Answer: X"
    match = re.search(r'(?:Resposta|Answer):\s*(\d+)', texto, re.IGNORECASE)
    if match:
        return int(match.group(1))
    
    # Fallback: último número encontrado
    numeros = re.findall(r'\d+', texto)
    return int(numeros[-1]) if numeros else None

# Exemplo de uso
problema = """
Roger tem 5 bolas de tênis. Ele compra 2 latas com 3 bolas cada.
Quantas bolas ele tem agora?
"""

resposta, confianca = self_consistency_cot(problema, n_samples=5)

if resposta:
    print(f"\nResposta final: {resposta} bolas (confiança: {confianca:.1%})")

Melhorias esperadas:

No benchmark GSM8K, self-consistency oferece +6% de acurácia versus CoT simples, filtra erros aleatórios através de voting, e fornece medida de confiança através da proporção de votos.

Exemplo 4: ReAct Agent Completo

Agente ReAct com registry de ferramentas e loop Thought-Action-Observation.

# uv pip install re typing

from typing import Dict, Callable, List, Tuple
import re

class ReActAgent:
    """
    Agente ReAct com registry de ferramentas.
    """

    def __init__(self, tools: Dict[str, Callable]):
        """
        Args:
            tools: Dicionário {nome_ferramenta: função_executável}
        """
        self.tools = tools
        self.history = []

    def run(self, question: str, max_iterations: int = 7) -> str:
        """
        Loop Thought-Action-Observation até resposta final.

        Args:
            question: Pergunta do usuário
            max_iterations: Limite de iterações para evitar loops infinitos

        Returns:
            Resposta final ou mensagem de timeout
        """
        prompt = self._build_initial_prompt(question)

        for i in range(max_iterations):
            print(f"\n{'='*60}")
            print(f"Iteração {i+1}")
            print(f"{'='*60}")
            
            # Gera Thought + Action
            response = llm.generate(prompt, temperature=0.0)

            # Parse do output
            thought, action, action_input = self._parse_response(response)

            # Registra thought
            self.history.append(f"Thought {i+1}: {thought}")
            print(f"Thought: {thought}")

            # Verifica se é resposta final
            if self._is_final_answer(thought):
                answer = self._extract_answer(response)
                print(f"\n✅ Resposta Final: {answer}")
                return answer

            # Executa ação
            if action not in self.tools:
                observation = f"Erro: ferramenta '{action}' não existe"
            else:
                try:
                    observation = self.tools[action](action_input)
                except Exception as e:
                    observation = f"Erro ao executar {action}: {str(e)}"

            # Registra ação e observação
            self.history.append(f"Action {i+1}: {action}[{action_input}]")
            self.history.append(f"Observation {i+1}: {observation}")
            
            print(f"Action: {action}[{action_input}]")
            print(f"Observation: {observation}")

            # Atualiza prompt com nova observação
            prompt = self._update_prompt(question, self.history)

        return "Limite de iterações atingido sem resposta final."

    def _build_initial_prompt(self, question: str) -> str:
        """Constrói prompt inicial com descrição de ferramentas."""
        tools_desc = "\n".join([
            f"- {name}: {func.__doc__}"
            for name, func in self.tools.items()
        ])

        return f"""Answer the following question using available tools.

Available Tools:
{tools_desc}

Use this format:
Thought: [your reasoning about next step]
Action: [tool_name]
Action Input: [input for the tool]
Observation: [result will be provided]
... (repeat Thought/Action/Observation as needed)
Thought: [final reasoning]
Answer: [final answer to the user]

Question: {question}

Thought:"""

    def _parse_response(self, response: str) -> Tuple[str, str, str]:
        """
        Extrai Thought, Action e Action Input do response.

        Returns:
            (thought, action_name, action_input)
        """
        thought_match = re.search(r'Thought:\s*(.+?)(?=\nAction:|\nAnswer:|$)',
                                 response, re.DOTALL)
        action_match = re.search(r'Action:\s*(\w+)', response)
        input_match = re.search(r'Action Input:\s*(.+?)(?=\n|$)', response)

        thought = thought_match.group(1).strip() if thought_match else ""
        action = action_match.group(1).strip() if action_match else ""
        action_input = input_match.group(1).strip() if input_match else ""

        return thought, action, action_input

    def _is_final_answer(self, thought: str) -> bool:
        """Detecta se thought indica resposta final."""
        final_indicators = [
            "resposta final",
            "final answer",
            "concluí",
            "tenho a resposta",
            "respondido"
        ]
        return any(ind in thought.lower() for ind in final_indicators)

    def _extract_answer(self, response: str) -> str:
        """Extrai resposta final do response."""
        match = re.search(r'Answer:\s*(.+)', response, re.DOTALL)
        return match.group(1).strip() if match else response

    def _update_prompt(self, question: str, history: List[str]) -> str:
        """Reconstrói prompt com histórico completo."""
        history_text = "\n".join(history)
        return f"""{self._build_initial_prompt(question)}

{history_text}

Thought:"""

# Definir ferramentas
def calculator(expression: str) -> float:
    """Avalia expressões matemáticas simples. Ex: calculator("2 + 3 * 4")"""
    try:
        # ATENÇÃO: eval é inseguro em produção! Este é apenas um exemplo.
        return eval(expression)
    except Exception as e:
        return f"Erro na expressão: {e}"

def search_database(query: str) -> List[Dict]:
    """Busca produtos no database. Ex: search_database("categoria:eletrônicos")"""
    # Simulação - em produção seria chamada real a database
    mock_db = {
        "eletrônicos": [
            {"nome": "Mouse", "preco": 50},
            {"nome": "Teclado", "preco": 80},
            {"nome": "Headset", "preco": 120}
        ]
    }

    # Parse simples de query
    if "categoria:" in query:
        categoria = query.split("categoria:")[1].strip()
        return mock_db.get(categoria, [])

    return []

# Exemplo de uso
agent = ReActAgent(tools={
    "calculator": calculator,
    "search_database": search_database
})

resultado = agent.run(
    "Quanto custaria comprar 5 unidades do produto mais barato de eletrônicos?"
)

print(f"\n{'='*60}")
print(f"RESULTADO FINAL: {resultado}")
print(f"{'='*60}")

Funcionalidades:

O agente implementa loop iterativo com ferramentas externas, parsing robusto de Thought/Action/Observation, detecção automática de resposta final, e histórico completo para debugging.

Exemplo 5: Self-Reflection Loop

Sistema de autocorreção com refinamento iterativo baseado em autocrítica.

# uv pip install re

import re

def self_reflection_loop(
    question: str,
    max_iterations: int = 3,
    quality_threshold: float = 0.8
) -> str:
    """
    Loop de self-reflection com refinamento iterativo.

    Args:
        question: Pergunta original
        max_iterations: Máximo de refinamentos
        quality_threshold: Score mínimo para aceitar resposta (0-1)

    Returns:
        Resposta final refinada
    """
    # Geração inicial
    print(f"{'='*60}")
    print("GERAÇÃO INICIAL")
    print(f"{'='*60}\n")
    
    answer = llm.generate(f"Question: {question}\nAnswer:", temperature=0.7)
    print(f"Resposta inicial:\n{answer}\n")

    for i in range(max_iterations):
        print(f"{'='*60}")
        print(f"ITERAÇÃO {i+1} - REFLEXÃO E REFINAMENTO")
        print(f"{'='*60}\n")
        
        # Autocrítica
        reflection_prompt = f"""
You provided this answer:
{answer}

For the question: {question}

Critically evaluate your answer:
1. What is correct?
2. What is wrong or missing?
3. How could it be improved?
4. Rate quality 0-1:

Reflection:"""

        reflection = llm.generate(reflection_prompt, temperature=0.3)
        print(f"Autocrítica:\n{reflection}\n")

        # Extrai score de qualidade
        score = extract_quality_score(reflection)
        print(f"Score de qualidade: {score:.2f}")

        if score >= quality_threshold:
            print(f"\nQualidade satisfatória alcançada!")
            return answer

        # Refinamento
        print(f"\nRefinando resposta...")
        
        refinement_prompt = f"""
Original Question: {question}

Your previous answer:
{answer}

Your self-critique:
{reflection}

Provide an improved answer addressing the issues you identified:

Refined Answer:"""

        answer = llm.generate(refinement_prompt, temperature=0.7)
        print(f"\nResposta refinada:\n{answer}\n")

    print(f"\n⚠️ Limite de iterações atingido")
    return answer

def extract_quality_score(reflection: str) -> float:
    """Extrai score numérico de 0-1 da reflection."""
    # Busca por "Rate quality: 0.X" ou "Quality: X/10"
    match = re.search(r'(?:quality|score)[:\s]+([0-9.]+)',
                     reflection.lower())
    if match:
        score = float(match.group(1))
        # Normaliza para 0-1 se necessário
        return score if score <= 1.0 else score / 10.0
    return 0.5  # Default se não encontrar score

# Exemplo de uso
pergunta = "Explique o teorema de Pitágoras para uma criança de 10 anos."

resposta_final = self_reflection_loop(
    question=pergunta,
    max_iterations=3,
    quality_threshold=0.8
)

print(f"\n{'='*60}")
print("RESPOSTA FINAL")
print(f"{'='*60}\n")
print(resposta_final)

Benefícios:

O sistema melhora qualidade através de autocrítica iterativa, utiliza score de qualidade para decisão de quando parar, e é ideal para tarefas criativas ou com critérios subjetivos.