Construindo agente autônomo com tool use em Python sem frameworks

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.

Código Python para construção de agentes autônomos com function calling

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:

  1. Você declara ferramentas — funções com nome, descrição e parâmetros tipados (como um contrato de API)
  2. O modelo decide quando chamar — ele analisa a pergunta do usuário e, se precisar de informação externa, escolhe uma ferramenta
  3. Você executa e devolve — o modelo não executa nada; ele te diz o que quer, você roda e entrega o resultado
  4. 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:

  1. Pergunta ao modelo
  2. Se ele pediu ferramentas, executa e devolve
  3. Repete até ele ter a resposta completa

Testando na Prática

Conceito de inteligência artificial e agentes autônomos

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:

  1. Rate limiting — limite chamadas por usuário/minuto
  2. Logging estruturado — cada tool call, cada resposta, cada erro
  3. Fallback — se a API cair, tenha uma resposta graceful
  4. Sandbox de execução — nunca rode código arbitrário sem isolamento
  5. Cache — se alguém pergunta o preço de PETR4, cache por 1 minuto
  6. 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.

Posts Similares