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
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:
- Princípios de Estruturação: Anatomia de prompts efetivos e padrões de organização
- In-Context Learning: Técnicas zero-shot, few-shot e many-shot para adaptação sem fine-tuning
- Chain-of-Thought Reasoning: Métodos para raciocínio complexo multi-etapa
- 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 |
As avaliações desta tabela baseiam-se em:
- Legibilidade Humana: Análise qualitativa de facilidade de leitura/edição
- Parsing Programático: Complexidade de implementação (regex vs. parsers nativos)
- Hierarquia Complexa: Capacidade de representar estruturas aninhadas (3+ níveis)
- Eficiência de Tokens: Medições empíricas em prompts equivalentes (seção seguinte)
- Robustez (Geração): Taxa de erros sintáticos observados em outputs de GPT-4/Claude (dados internos, não publicados)
- 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 |
Para a maioria dos casos de uso em agentes de IA:
- Comece com Markdown (simplicidade, tokens, legibilidade)
- Migre para JSON se precisar de parsing robusto ou integração com APIs
- 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:
- Interpreta a instrução “Classifique o sentimento”
- Mapeia para padrões similares vistos em treinamento (reviews, análises de sentimento em textos)
- 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:
- Tarefas bem estabelecidas: Tradução, sumarização, Q&A factual
- Vocabulário comum: Tarefas descritas em termos familiares ao modelo
- Baixa ambiguidade: Critérios de decisão objetivos e universais
- Restrições de latência: Minimizar tokens de input reduz tempo de inferência
Limitações conhecidas:
- Inconsistência de formato: Sem exemplos, formato de saída pode variar
- Interpretação ambígua: Instruções podem ser mal interpretadas
- Conhecimento de cauda longa: Tarefas de domínio específico falham frequentemente
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]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)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 |
Many-shot é economicamente viável apenas quando:
- Volumetria baixa: < 1000 chamadas/dia (custo de fine-tuning não amortiza)
- Dados em evolução: Exemplos mudam frequentemente (fine-tuning seria retreinado constantemente)
- 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) |
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.
Como usar a árvore:
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)
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%.
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.
Valide com dados reais: Esta árvore é heurística. Sempre teste com seu dataset e métricas específicas antes de escalar para produção.
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:
- 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
- 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_finalPara 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 = 4Por que self-consistency funciona:
- Diversidade de caminhos:
temperature=0.7faz o modelo gerar raciocínios diferentes - Erros não-sistemáticos: Erros aleatórios aparecem em apenas algumas cadeias
- Voting filtra outliers: Resposta correta tende a aparecer mais vezes
- 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:
- Raciocinem sobre qual ação tomar (Thought)
- Executem ações externas (Action) — chamar APIs, buscar databases, usar ferramentas
- Observem os resultados (Observation)
- 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."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:
- Loops infinitos: Agente pode repetir mesmas ações (mitigação: limite de iterações, detecção de loops)
- Parsing frágil: LLM pode não seguir formato exato (mitigação: retry com correção do formato)
- 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:
- Gera resposta inicial
- Critica a própria resposta (identifica erros, lacunas)
- Refina baseado na crítica
- 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 answerPara 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:
- Tarefas criativas: Escrita, design de prompts, geração de código
- Respostas complexas: Onde primeira tentativa raramente é ideal
- Critérios subjetivos: Qualidade de explicação, adequação ao público-alvo
- 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-checkingAnalogical 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:
- Recordar problemas similares que ele já sabe resolver
- Gerar soluções para esses problemas análogos
- 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?
- Ativa conhecimento latente: Modelo “lembra” padrões de solução similares em seus pesos
- Generalização: Em vez de memorizar exemplos fixos, modelo identifica estrutura abstrata do problema
- 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 solutionQuando usar analogical prompting:
- Domínios com padrões claros: Matemática, física, programação
- Quando você não tem exemplos prontos: Modelo gera as próprias analogias
- Problemas estruturalmente similares a conhecidos: Transferência funciona melhor
- Reduzir custo de few-shot: Menos tokens que fornecer 5-10 exemplos completos
Limitações:
- Analogias podem ser inadequadas: Modelo pode gerar problemas não-análogos
- Menos controle: Você não escolhe os exemplos (modelo escolhe)
- 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:
- Few-Shot Dynamic Classifier: Classificador com seleção dinâmica de exemplos por similaridade
- Chain-of-Thought Math Solver: Comparação entre zero-shot CoT, few-shot CoT e self-consistency
- 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.