Objetivo da Aula
Ao concluir esta aula, você será capaz de:
Descrever os 4 componentes de um agente autônomo (memória, planner, executor, toolbox) e suas responsabilidades
Implementar um agent loop completo em Python com Anthropic SDK
Executar tools baseadas em tool_use blocks do response do Claude
Controlar o loop com stop_reason e limite de iterações
Distinguir quando usar agente vs chatbot vs workflow fixo
Por que isso importa
Agentes autônomos são fundamentalmente diferentes de chatbots.
Um chatbot responde perguntas. Um agente decompõe tarefas, decide quais ações tomar, executa essas ações e itera até atingir o objetivo — sem que o humano precise supervisionar cada passo.
A diferença não é filosófica, é arquitetural. Um chatbot tem o loop:
user_input → LLM → respostaUm agente tem o loop:
task → LLM → tool_use → execute → LLM → tool_use → execute → ... → respostaEsse loop transforma um LLM de “respondedor passivo” para “executor ativo”. E é exatamente isso que o mercado está comprando hoje: não modelos mais inteligentes, mas sistemas que fazem coisas de verdade.
O custo de uma chamada de agente com 5 iterações é ~5x o custo de uma resposta simples. Isso é real e precisa entrar no cálculo de produto. Mas o valor entregue também é ~5-50x maior.
Conceitos Fundamentais
Os 4 Componentes de um Agente
1. Memória O que o agente sabe e lembra. No starter, a memória de curta duração é a lista mensagens — a conversa acumulada. A memória de longa duração é a lista notas — persiste além do loop atual.
2. Planner (LLM) O cérebro do agente. Decide o que fazer a seguir baseado no estado atual (mensagens). No starter, é a chamada a client.messages.create(). O Claude analisa a tarefa, consulta as tools disponíveis via TOOLS_SCHEMA, e decide: responder diretamente (end_turn) ou usar uma tool (tool_use).
3. Executor Executa as decisões do Planner. No starter, é executar_tool(nome, argumentos). Recebe o nome da tool e os argumentos que o Claude escolheu, e chama a função Python correspondente.
4. Toolbox O repertório de ações disponíveis. No starter, são 3 tools: calcular, buscar_web, salvar_nota. O TOOLS_SCHEMA é a interface entre o LLM (que precisa de JSON Schema) e as funções Python.
O Agent Loop: Linha a Linha
def agent_loop(tarefa: str, max_iteracoes: int = 10) -> str:
mensagens = [{'role': 'user', 'content': tarefa}]
iteracao = 0
while iteracao < max_iteracoes:
iteracao += 1
# 1. Planner decide
response = client.messages.create(
model='claude-haiku-4-5-20251001',
max_tokens=1000,
tools=TOOLS_SCHEMA,
messages=mensagens,
)
# 2. Verificar parada
if response.stop_reason == 'end_turn':
# Tarefa completa — extrair texto final
texto = next((b.text for b in response.content if hasattr(b, 'text')), '')
return texto
# 3. Processar tool calls
mensagens.append({'role': 'assistant', 'content': response.content})
tool_results = []
for block in response.content:
if block.type != 'tool_use':
continue
resultado = executar_tool(block.name, block.input)
tool_results.append({
'type': 'tool_result',
'tool_use_id': block.id,
'content': resultado,
})
# 4. Adicionar resultados e iterar
mensagens.append({'role': 'user', 'content': tool_results})
return 'Limite de iterações atingido.'4 etapas que se repetem: 1. Planner decide: client.messages.create() com tools=TOOLS_SCHEMA 2. Verificar parada: stop_reason == 'end_turn' → tarefa completa 3. Executar tools: para cada tool_use block, executar a função e coletar resultado 4. Iterar: adicionar os tool_result blocks ao histórico e chamar o LLM novamente
stop_reason: A Bifurcação Central
response.stop_reason # 'end_turn' ou 'tool_use'
'end_turn': o Claude decidiu que tem informação suficiente para responder. Loop termina.
'tool_use': o Claude quer usar uma ou mais tools antes de responder. Loop continua.Quando stop_reason == 'tool_use', response.content contém blocos tool_use como:
[
TextBlock(text="Vou calcular isso.", type='text'),
ToolUseBlock(id='toolu_abc', name='calcular', input={'expressao': '12 * 365'}, type='tool_use'),
]Note que pode haver um TextBlock antes do ToolUseBlock — o “raciocínio” do Claude. Você deve incluir todo response.content no histórico (não só os tool_use blocks).
Tool Results: Formato Correto
tool_results.append({
'type': 'tool_result',
'tool_use_id': block.id, # ← DEVE corresponder ao id do tool_use
'content': resultado, # string com o resultado
})
mensagens.append({'role': 'user', 'content': tool_results})O tool_use_id é crítico — o Claude usa para correlacionar qual resultado corresponde a qual tool call. Erro comum: passar block.name no lugar de block.id.
Segurança em calcular()
def calcular(expressao: str) -> str:
permitidos = set('0123456789+-*/.() ')
if not all(c in permitidos for c in expressao):
return 'Erro: expressão contém caracteres não permitidos'
resultado = eval(expressao)
return str(resultado)
eval() em código de produção é normalmente perigoso (RCE). Aqui é seguro por dois motivos: 1. Whitelist de caracteres — só permite dígitos e operadores matemáticos 2. Sem import, sem acesso a builtins com strings maliciosasEm produção, use sympy.sympify() ou ast.literal_eval() para parsing mais seguro.
Aprofundamento Técnico
Por que response.content (não só o texto) vai para o histórico
# ERRADO: perde os tool_use blocks
mensagens.append({'role': 'assistant', 'content': response.content[0].text})
# CORRETO: preserva todos os blocos
mensagens.append({'role': 'assistant', 'content': response.content})Quando o Claude chama uma tool, response.content contém uma lista com TextBlock + ToolUseBlock. Se você só adicionar o texto, na próxima iteração o Claude não vai ter o contexto dos tool_use blocks, e a correlação com os tool_result vai quebrar.
max_iteracoes como Guardrail de Custo
while iteracao < max_iteracoes: # default: 10Sem esse limite, um agente mal projetado pode entrar em loop infinito. Cada iteração custa tokens. 10 iterações com claude-haiku (~$0.00003/iteração) = ~$0.0003 por task. 1000 tasks/dia = $0.30/dia. Escalável.
Com claude-sonnet (~$0.003/iteração): 1000 tasks = $30/dia. O limite de iterações é também um safety valve financeiro.
Tipos de Agentes: Quando Usar Cada Um
Tipo | Descrição | Quando usar |
Reactive | percepção → ação sem plano | alertas simples, roteamento de mensagens |
Deliberative | planeja antes de agir | tarefas complexas, múltiplos passos |
Hybrid | reactive para urgências + deliberative para planejamento | sistemas de produção |
Single-agent | 1 LLM + toolbox | maioria dos casos |
Multi-agent | múltiplos LLMs colaborando | decomposição de tarefas muito complexas |
Exemplos Anotados
Exemplo 1: Agent Loop Completo (solução dos TODOs)
def agent_loop(tarefa: str, max_iteracoes: int = 10) -> str:
print(f'\n🤖 AGENTE INICIADO')
print(f'Tarefa: {tarefa}\n')
mensagens = [{'role': 'user', 'content': tarefa}]
iteracao = 0
while iteracao < max_iteracoes:
iteracao += 1
print(f'--- Iteração {iteracao}/{max_iteracoes} ---')
# TODO 1: chamada de API
response = client.messages.create(
model='claude-haiku-4-5-20251001',
max_tokens=1000,
tools=TOOLS_SCHEMA,
messages=mensagens,
)
# Mostrar raciocínio textual se presente
for block in response.content:
if hasattr(block, 'text') and block.text:
print(f'💭 {block.text[:200]}')
# TODO 2: verificar stop_reason
if response.stop_reason == 'end_turn':
# Tarefa completa
texto = next((b.text for b in response.content if hasattr(b, 'text')), '')
print(f'\n✅ Concluído em {iteracao} iteração(ões)')
return texto
if response.stop_reason == 'tool_use':
# Adicionar resposta completa (inclui text + tool_use blocks)
mensagens.append({'role': 'assistant', 'content': response.content})
tool_results = []
for block in response.content:
if block.type != 'tool_use':
continue
print(f'🔧 Chamando: {block.name}({block.input})')
resultado = executar_tool(block.name, block.input)
print(f' → {resultado[:100]}')
tool_results.append({
'type': 'tool_result',
'tool_use_id': block.id, # ← id, não name
'content': resultado,
})
mensagens.append({'role': 'user', 'content': tool_results})
print(f'⚠️ Limite de {max_iteracoes} iterações atingido.')
return 'Tarefa não concluída dentro do limite de iterações.'Exemplo 2: Trace Completo de Execução
Para a tarefa “Pesquise o que é RAG, calcule 12 * 365 e salve uma nota”:
Iteração 1:
💭 Vou pesquisar RAG primeiro.
🔧 buscar_web({'query': 'RAG'})
→ RAG (Retrieval-Augmented Generation) combina busca semântica com LLMs...
Iteração 2:
💭 Agora o cálculo.
🔧 calcular({'expressao': '12 * 365'})
→ 4380
Iteração 3:
💭 Salvando a nota com os resultados.
🔧 salvar_nota({'titulo': 'RAG e Cálculo', 'conteudo': 'RAG é... 12*365=4380'})
→ Nota #1 salva: RAG e Cálculo
Iteração 4 (end_turn):
✅ Concluído. RAG é um padrão que combina busca com LLMs.
12*365=4380 dias. Nota salva com sucesso.Padrões e Armadilhas
Padrões
Padrão 1: executar_tool() com dict de funções
TOOL_FUNCTIONS = {
'calcular': calcular,
'buscar_web': buscar_web,
'salvar_nota': salvar_nota,
}
def executar_tool(nome: str, argumentos: dict) -> str:
if nome not in TOOL_FUNCTIONS:
return f'Erro: tool "{nome}" não encontrada'
return TOOL_FUNCTIONS[nome](**argumentos)Evita if/elif aninhados. Adicionar nova tool = adicionar ao dict.
Padrão 2: Tools retornam str, sempre O content de um tool_result deve ser string. Se sua função retorna dict ou list, serialize:
return json.dumps(resultado, ensure_ascii=False)Padrão 3: Incluir todo response.content no histórico
mensagens.append({'role': 'assistant', 'content': response.content})Não filtre — o Claude precisa ver os tool_use blocks que ele mesmo gerou.
Armadilhas
⚠️ Armadilha 1: tool_use_id vs tool_name
# ERRADO: quebra correlação
tool_results.append({'type': 'tool_result', 'tool_use_id': block.name, ...})
# CORRETO: usar block.id
tool_results.append({'type': 'tool_result', 'tool_use_id': block.id, ...})⚠️ Armadilha 2: Sem limite de iterações
# PERIGO: pode rodar para sempre (e consumir budget)
while True:
response = client.messages.create(...)
# SEGURO: sempre limite
while iteracao < max_iteracoes:
iteracao += 1
...⚠️ Armadilha 3: Tool que lança exceção quebra o loop
# RUIM: exceção propaga para cima e mata o agente
def buscar_web(query):
return requests.get(url).json()['result']
# BOM: capturar e retornar erro como string
def buscar_web(query):
try:
return requests.get(url, timeout=5).json()['result']
except Exception as e:
return f'Erro ao buscar: {e}'O Claude pode se recuperar de erros em tools se receber a mensagem de erro como tool_result. Exceções não tratadas matam o processo.
Se não for realizar o laboratório, pule para o próximo capítulo.
Ponte para o Lab
O starter tem 2 TODOs:
TODO 1 (linha response = None): substitua por:
response = client.messages.create(
model='claude-haiku-4-5-20251001',
max_tokens=1000,
tools=TOOLS_SCHEMA,
messages=mensagens,
)TODO 2 (verificar stop_reason): após a chamada de API:
if response.stop_reason == 'end_turn':
texto = next((b.text for b in response.content if hasattr(b, 'text')), '')
return texto
mensagens.append({'role': 'assistant', 'content': response.content})
tool_results = []
for block in response.content:
if block.type != 'tool_use':
continue
resultado = executar_tool(block.name, block.input)
tool_results.append({'type': 'tool_result', 'tool_use_id': block.id, 'content': resultado})
mensagens.append({'role': 'user', 'content': tool_results})A seção “Conceitos Fundamentais” explica cada linha. O Exemplo 1 mostra a solução completa com prints de debug.
Agora você está pronto para o lab.