Como Construir um Agente Autônomo com Tool Use (Sem Framework, Só Python + API)
Eu gastei 3 semanas tentando fazer um agente de IA funcionar. Primeiro tentei LangChain — zilhão de abstrações, documentação que parece escrita por um conspirador. Depois tentei CrewAI — bonito no demo, pesadelo na produção. No final, joguei tudo fora e escrevi 200 linhas de Python puro.
Funcionou. E eu vou te mostrar exatamente como.
Se você quer construir um agente autônomo com tool use — aquele que realmente faz coisas ao invés de só gerar texto — este guia é para você. Sem framework. Sem mágica. Só Python, uma API de LLM e a estratégia certa.

O Problema dos Frameworks de Agentes
Vamos ser honestos: a maioria dos tutoriais de “agentes de IA” na internet te ensina a usar um framework que faz tudo por você. O problema? Quando quebra — e vai quebrar — você não faz ideia do que aconteceu.
Eu passei por isso. Meu agente em LangChain simplesmente parou de chamar ferramentas após a terceira iteração. Debuguei por 2 dias. O problema era um prompt template que o framework injetava silenciosamente, modificando o comportamento do sistema.
A lição: se você não entende o mecanismo, não pode consertar quando falha.
Como Tool Use Funciona (O Mecanismo Real)
Antes de escrever código, você precisa entender o que acontece por baixo dos panos. O tool use (ou function calling) não é mágica — é um protocolo de comunicação entre você e o modelo:
- Você declara ferramentas — funções com nome, descrição e parâmetros tipados (como um contrato de API)
- O modelo decide quando chamar — ele analisa a pergunta do usuário e, se precisar de informação externa, escolhe uma ferramenta
- Você executa e devolve — o modelo não executa nada; ele te diz o que quer, você roda e entrega o resultado
- O modelo refina — com o resultado em mãos, ele pode responder ou pedir mais ferramentas
Isso se chama loop de raciocínio (reasoning loop). E é exatamente isso que os frameworks abstraem demais.
O Formato das Ferramentas
Toda ferramenta é um JSON Schema. O modelo recebe isso junto com o prompt:
{
"name": "buscar_preco_acao",
"description": "Busca o preço atual de uma ação na bolsa",
"parameters": {
"type": "object",
"properties": {
"ticker": {
"type": "string",
"description": "Código da ação, ex: PETR4.SA"
}
},
"required": ["ticker"]
}
}
O modelo vê isso e pensa: “Hmm, o usuário perguntou sobre PETR4. Eu tenho uma ferramenta que busca preço de ações. Vou usá-la com o parâmetro ticker=’PETR4.SA'”.
Implementando o Agente do Zero
Vamos construir um agente que pode buscar preços de ações, consultar o clima e fazer cálculos. Três ferramentas simples para demonstrar o padrão.
Passo 1: Definindo as Ferramentas
import json
import requests
from datetime import datetime
# Registro de ferramentas disponíveis
TOOLS = [
{
"type": "function",
"function": {
"name": "buscar_preco_acao",
"description": "Busca o preço atual de uma ação na B3",
"parameters": {
"type": "object",
"properties": {
"ticker": {
"type": "string",
"description": "Código da ação (ex: PETR4.SA, VALE3.SA)"
}
},
"required": ["ticker"]
}
}
},
{
"type": "function",
"function": {
"name": "consultar_clima",
"description": "Retorna o clima atual de uma cidade",
"parameters": {
"type": "object",
"properties": {
"cidade": {
"type": "string",
"description": "Nome da cidade"
}
},
"required": ["cidade"]
}
}
},
{
"type": "function",
"function": {
"name": "calcular",
"description": "Executa um cálculo matemático",
"parameters": {
"type": "object",
"properties": {
"expressao": {
"type": "string",
"description": "Expressão matemática (ex: '150 * 0.15 + 150')"
}
},
"required": ["expressao"]
}
}
}
]
Notou? Cada ferramenta é auto-explicativa. O modelo precisa do name para saber como chamar, description para decidir quando chamar, e parameters para saber o que passar.
Passo 2: Implementando as Funções Reais
# Mapeamento nome -> função executável
FUNCTION_MAP = {
"buscar_preco_acao": lambda args: buscar_preco(args["ticker"]),
"consultar_clima": lambda args: consultar_clima(args["cidade"]),
"calcular": lambda args: calcular(args["expressao"]),
}
def buscar_preco(ticker: str) -> str:
"""Busca preço via Yahoo Finance (sem dependency)."""
url = f"https://query1.finance.yahoo.com/v8/finance/chart/{ticker}"
resp = requests.get(url, timeout=10)
data = resp.json()
result = data["chart"]["result"][0]["meta"]
preco = result["regularMarketPrice"]
moeda = result.get("currency", "BRL")
return json.dumps({
"ticker": ticker,
"preco": preco,
"moeda": moeda,
"timestamp": datetime.now().isoformat()
})
def consultar_clima(cidade: str) -> str:
"""Consulta clima via wttr.in (grátis, sem API key)."""
url = f"https://wttr.in/{cidade}?format=j1"
resp = requests.get(url, timeout=10)
data = resp.json()
atual = data["current_condition"][0]
return json.dumps({
"cidade": cidade,
"temperatura": f"{atual['temp_C']}°C",
"sensacao": f"{atual['FeelsLikeC']}°C",
"descricao": atual["weatherDesc"][0]["value"],
"umidade": f"{atual['humidity']}%"
})
def calcular(expressao: str) -> str:
"""Executa cálculo de forma segura."""
# PERIGO: em produção, use um sandbox real!
# Por agora, permitimos apenas números e operadores básicos
import re
if not re.match(r'^[\d\s\+\-\*\/\.\(\)]+$', expressao):
return json.dumps({"erro": "Expressão inválida"})
try:
resultado = eval(expressao) # noqa: S307
return json.dumps({"expressao": expressao, "resultado": resultado})
except Exception as e:
return json.dumps({"erro": str(e)})
Cada função retorna JSON. Isso é importante — o modelo precisa de dados estruturados para raciocinar sobre eles.
🚨 Box do Perrengue: Eu usei
eval()ali. Na primeira versão do meu agente, não sanitizei a entrada. Um usuário digitou__import__('os').system('rm -rf /')como “expressão matemática”. O servidor não era meu, mas o susto foi real. Em produção, use um parser matemático de verdade ou um sandbox. A regex acima é o mínimo aceitável para protótipos.
Passo 3: O Loop de Raciocínio
Aqui está o coração do agente — o loop que faz tudo funcionar:
from openai import OpenAI
client = OpenAI(api_key="sua-chave-aqui")
MODEL = "gpt-4o" # ou qualquer modelo com function calling
def rodar_agente(mensagem_usuario: str, max_iteracoes: int = 5) -> str:
"""
Executa o agente com loop de raciocínio.
O agente pode:
1. Responder diretamente (se não precisa de ferramentas)
2. Chamar ferramentas (pode chamar múltiplas de uma vez)
3. Refinar a resposta após receber resultados
"""
mensagens = [
{
"role": "system",
"content": (
"Você é um assistente financeiro e meteorológico brasileiro. "
"Use as ferramentas disponíveis para responder com dados reais. "
"Responda sempre em português do Brasil. "
"Quando fizer cálculos, mostre o passo a passo."
)
},
{"role": "user", "content": mensagem_usuario}
]
for iteracao in range(max_iteracoes):
print(f"--- Iteração {iteracao + 1} ---")
# 1. Chama o modelo
resposta = client.chat.completions.create(
model=MODEL,
messages=mensagens,
tools=TOOLS,
tool_choice="auto" # modelo decide quando usar
)
msg = resposta.choices[0].message
# 2. Se não tem tool calls, terminamos
if not msg.tool_calls:
return msg.content
# 3. Adiciona resposta do assistente ao histórico
mensagens.append(msg)
# 4. Executa cada ferramenta solicitada
for tool_call in msg.tool_calls:
nome = tool_call.function.name
args = json.loads(tool_call.function.arguments)
print(f" 🔧 Chamando: {nome}({args})")
# Executa a função
if nome in FUNCTION_MAP:
resultado = FUNCTION_MAP[nome](args)
else:
resultado = json.dumps({"erro": f"Ferramenta '{nome}' não existe"})
print(f" 📋 Resultado: {resultado[:100]}...")
# 5. Devolve resultado ao modelo
mensagens.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": resultado
})
return "⚠️ Limite de iterações atingido. O agente precisou de mais passos."
Pronto. Isso é um agente autônomo funcional em ~100 linhas. O loop é simples:
- Pergunta ao modelo
- Se ele pediu ferramentas, executa e devolve
- Repete até ele ter a resposta completa
Testando na Prática

Vamos ver o agente em ação com perguntas que exigem combinação de ferramentas:
# Teste 1: Pergunta simples (1 ferramenta)
resultado = rodar_agente("Qual o preço da Petrobras agora?")
print(resultado)
# Teste 2: Pergunta composta (2+ ferramentas)
resultado = rodar_agente(
"Quanto custa comprar 100 ações da Vale? "
"E como está o clima em São Paulo pra eu decidir "
"se vou ao escritório ou trabalho de casa?"
)
print(resultado)
# Teste 3: Pergunta que precisa de cálculo
resultado = rodar_agente(
"Se eu comprar 50 ações da PETR4 e 100 da VALE3, "
"qual o total investido? Calcule considerando os preços atuais."
)
print(resultado)
No Teste 3, algo bonito acontece: o modelo chama buscar_preco_acao duas vezes (PETR4 e VALE3), depois chama calcular com os resultados. Três iterações de raciocínio autônomo, zero intervenção humana.
Adicionando Memória ao Agente
Um agente sem memória é como um peixe dourado. Vamos adicionar contexto persistente:
class AgenteComMemoria:
def __init__(self, system_prompt: str):
self.historico = [
{"role": "system", "content": system_prompt}
]
self.ferramentas_disponiveis = TOOLS
def chat(self, mensagem: str) -> str:
self.historico.append({"role": "user", "content": mensagem})
resposta = client.chat.completions.create(
model=MODEL,
messages=self.historico,
tools=self.ferramentas_disponiveis,
tool_choice="auto"
)
msg = resposta.choices[0].message
if msg.tool_calls:
self.historico.append(msg)
for tc in msg.tool_calls:
args = json.loads(tc.function.arguments)
nome = tc.function.name
resultado = FUNCTION_MAP[nome](args)
self.historico.append({
"role": "tool",
"tool_call_id": tc.id,
"content": resultado
})
# Recursão com contexto atualizado
return self._processar_resposta()
self.historico.append({"role": "assistant", "content": msg.content})
return msg.content
def _processar_resposta(self) -> str:
resposta = client.chat.completions.create(
model=MODEL,
messages=self.historico,
tools=self.ferramentas_disponiveis,
tool_choice="auto"
)
msg = resposta.choices[0].message
if msg.tool_calls:
self.historico.append(msg)
for tc in msg.tool_calls:
args = json.loads(tc.function.arguments)
resultado = FUNCTION_MAP[tc.function.name](args)
self.historico.append({
"role": "tool",
"tool_call_id": tc.id,
"content": resultado
})
return self._processar_resposta()
self.historico.append({"role": "assistant", "content": msg.content})
return msg.content
# Uso com memória entre turnos
agente = AgenteComMemoria(
"Você é um consultor financeiro brasileiro. "
"Lembre do que conversamos. Use dados reais."
)
print(agente.chat("Qual o preço da PETR4?"))
# ... mais tarde ...
print(agente.chat("E aquela PETR4 que perguntei antes, subiu ou desceu?"))
# O agente LEMBRA do contexto anterior!
Erros Que Vão te Pegar (e Como se Defender)
1. Loop Infinito de Ferramentas
Às vezes o modelo entra em loop: chama a mesma ferramenta repetidamente com argumentos ligeiramente diferentes. Isso consome tokens e dinheiro.
Solução: Sempre use max_iteracoes. E logue cada chamada para detectar padrões.
# Detecção de loop
def detectar_loop(historico_chamadas, janela=3):
"""Detecta se as últimas N chamadas são iguais."""
if len(historico_chamadas) < janela:
return False
ultimas = historico_chamadas[-janela:]
return len(set(ultimas)) == 1 # todas iguais = loop
2. Parâmetros Malformados
O modelo pode passar "150" como string quando a função espera 150 como número. Ou inventar parâmetros que não existem.
Solução: Valide e normalize tudo dentro das suas funções.
def validar_args(nome_funcao: str, args: dict) -> dict:
"""Normaliza e valida argumentos antes de executar."""
if nome_funcao == "buscar_preco_acao":
ticker = args.get("ticker", "").upper()
if not ticker.endswith(".SA"):
ticker += ".SA" # sufixo B3 no Yahoo Finance
return {"ticker": ticker}
return args
3. Custos Fora de Controle
Cada iteração do loop consome tokens. Um agente rodando a cada minuto pode gerar uma conta salgada.
Solução: Monitore uso com um decorator:
import functools
import time
def monitorar_custo(func):
"""Decorator para rastrear uso de tokens."""
stats = {"chamadas": 0, "tokens_estimados": 0}
@functools.wraps(func)
def wrapper(*args, **kwargs):
inicio = time.time()
resultado = func(*args, **kwargs)
duracao = time.time() - inicio
stats["chamadas"] += 1
# Estimativa grosseira: ~500 tokens por chamada
stats["tokens_estimados"] += 500
print(f"📊 Stats: {stats['chamadas']} chamadas, "
f"~{stats['tokens_estimados']} tokens, "
f"última: {duracao:.1f}s")
return resultado
return wrapper
Quando Frameworks Fazem Sentido
Não sou contra frameworks. Sou contra usá-los sem entender o que estão fazendo. Depois de implementar na mão, você vai olhar para LangChain e pensar "ah, então é isso que o AgentExecutor faz" — e vai poder debugar em minutos ao invés de dias.
Use frameworks quando:
- Já entende o mecanismo fundamental e quer acelerar
- Precisa de integrações complexas (vector stores, múltiplos modelos, tracing)
- Seu agente escala para dezenas de ferramentas
Evite quando:
- Está aprendendo o conceito
- O agente é simples (menos de 10 ferramentas)
- Precisa de controle fino sobre o comportamento
- O prazo é curto e você não quer surpresas
De Protótipo a Produção
Para levar esse agente do script para algo real, você precisa:
- Rate limiting — limite chamadas por usuário/minuto
- Logging estruturado — cada tool call, cada resposta, cada erro
- Fallback — se a API cair, tenha uma resposta graceful
- Sandbox de execução — nunca rode código arbitrário sem isolamento
- Cache — se alguém pergunta o preço de PETR4, cache por 1 minuto
- Observabilidade — trace cada execução (use LangSmith ou similar)
# Cache simples com TTL
from functools import lru_cache
from time import time
_cache = {}
def cached_get(url: str, ttl: int = 60) -> dict:
"""Cache HTTP GET com TTL em segundos."""
agora = time.time()
if url in _cache:
resultado, timestamp = _cache[url]
if agora - timestamp < ttl:
return resultado
resp = requests.get(url, timeout=10)
resultado = resp.json()
_cache[url] = (resultado, agora)
return resultado
O Que Eu Aprendi da Pior Forma
Meu primeiro agente em produção custou R$ 340 em tokens na primeira semana. Por quê? Porque eu não limpei o histórico entre sessões. O histórico crescia a cada pergunta, e o modelo processava tudo a cada iteração. Depois de 20 mensagens, cada resposta custava o equivalente a 5 cafés.
Solução: Janela deslizante + sumarização:
def limpar_historico(historico, max_mensagens=20):
"""Mantém as últimas N mensagens + system prompt."""
if len(historico) <= max_mensagens:
return historico
system = [h for h in historico if h["role"] == "system"]
conversa = [h for h in historico if h["role"] != "system"]
# Pega as últimas mensagens
recentes = conversa[-max_mensagens:]
return system + recentes
Ou, se quiser ser mais sofisticado, use o próprio modelo para sumarizar o histórico antigo antes de descartar:
def sumarizar_historico(historico_antigo: list) -> str:
"""Usa o modelo para comprimir histórico."""
resumo = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": "Resuma esta conversa em 2-3 parágrafos, mantendo dados e decisões-chave."},
{"role": "user", "content": str(historico_antigo)}
]
)
return resumo.choices[0].message.content
Próximos Passos
Com essa base, você pode evoluir em várias direções:
- Agente com planejamento — antes de agir, o modelo escreve um plano de execução
- Multi-agente — agentes especialistas que delegam entre si
- Aprendizado por feedback — armazene acertos e erros para melhorar futuras respostas
- Ferramentas dinâmicas — o agente registra novas ferramentas em runtime
O caminho é esse: entenda o mecânismo, construa na mão, depois abstraia. Se você pular direto pro framework, vai ficar dependendo de Stack Overflow toda vez que algo der errado. E acredite — vai dar errado.
E você? Qual automação com agentes de IA você quer construir? Um assistente que monitora seus investimentos? Um que organiza sua caixa de email? Um que faz deploy automático quando os testes passam? Conta nos comentários — talora eu construa junto contigo no próximo post.
Se chegou até aqui, compartilha com aquele amigo que vive falando de "agentes de IA" mas nunca construiu um. Chegou a hora dele sujar as mãos.
