Existe uma indústria inteira vendendo frameworks de agente, e isso criou a impressão de que construir um é complicado. Não é. Um agente de IA é um laço: o modelo escreve o que quer fazer, o seu código executa, devolve o resultado e chama o modelo de novo. Tudo o mais — memória, ferramentas, planejamento — é detalhe em cima disso.

Este tutorial constrói um agente funcional em Python puro, sem LangChain, sem CrewAI, sem dependência além do cliente HTTP. No fim você vai ter um agente que lê arquivos, roda contas, consulta uma API e sabe parar. E vai saber exatamente onde cada real foi gasto.

⚡ Resumo

Agente = laço modelo → ferramenta → modelo com condição de parada e teto de custo. 120 linhas de Python. Roda sobre a API por token da GPUBrasil (compatível com a da OpenAI), a partir de R$ 0,49 por milhão de tokens de entrada.

O que é um agente, sem marketing

Um LLM sozinho só faz uma coisa: recebe texto e devolve texto. Ele não lê o seu banco, não abre arquivo, não chama API. Um agente é o programa que dá esses braços a ele, com três peças:

É isso. Se você já escreveu um while com um switch dentro, você já escreveu 80% de um agente.

Passo 1: a conta e a chave

Vamos usar a API de inferência por token da GPUBrasil. Ela fala o mesmo protocolo da API da OpenAI, então qualquer biblioteca compatível funciona — só muda a URL base e a chave.

  1. Crie a conta em gpubrasil.com.br e adicione saldo (Pix, boleto ou cartão, em reais).
  2. No painel, vá em Chaves de API e gere uma chave. Ela começa com gpub_live_ e só aparece uma vez.
  3. Guarde em variável de ambiente — nunca no código.
export GPUBRASIL_API_KEY="gpub_live_suachaveaqui"
pip install openai

Teste em três linhas antes de escrever o agente. Se isto responder, o resto é só lógica:

import os
from openai import OpenAI

cliente = OpenAI(
    base_url="https://gpubrasil.com.br/v1",
    api_key=os.environ["GPUBRASIL_API_KEY"],
)

r = cliente.chat.completions.create(
    model="gpub-plus",
    messages=[{"role": "user", "content": "Responda apenas: ok"}],
)
print(r.choices[0].message.content)

Qual modelo escolher para um agente

Agente é o caso de uso que mais consome token: cada volta do laço reenvia todo o histórico. A escolha do modelo é a diferença entre R$ 3 e R$ 300 no fim do mês.

ModeloEntrada (1M)Saída (1M)Quando usar no agente
gpub-fastR$ 0,49R$ 1,09Classificar, extrair, roteirizar. O "operário" do laço.
gpub-miniR$ 0,59R$ 3,49Tarefas objetivas, alto volume.
gpub-plusR$ 0,69R$ 3,99Padrão recomendado. Bom em código e em seguir formato.
gpub-baseR$ 7,90R$ 17,90Quando o plus erra o plano com frequência.
gpub-maxR$ 17,90R$ 84,90Raciocínio longo, base de código inteira. Caro por volta.

Recomendação prática: comece no gpub-plus. Se o agente errar o formato, não suba de modelo antes de apertar o prompt — na maioria das vezes o problema é instrução vaga, não capacidade.

Passo 2: as ferramentas

Ferramenta é função Python comum. A única regra é que ela receba e devolva coisas serializáveis em texto, e que ela mesma valide a entrada — o modelo vai errar o argumento uma hora, e a ferramenta é a última linha de defesa.

import json, pathlib

RAIZ = pathlib.Path("./dados").resolve()   # o agente não sai daqui

def ler_arquivo(caminho: str) -> str:
    """Lê um arquivo de texto dentro de ./dados."""
    alvo = (RAIZ / caminho).resolve()
    if not alvo.is_relative_to(RAIZ):        # trava de path traversal
        return "ERRO: caminho fora da pasta permitida."
    if not alvo.exists():
        return f"ERRO: {caminho} não existe."
    return alvo.read_text(encoding="utf-8")[:8000]

def listar_arquivos() -> str:
    """Lista os arquivos disponíveis."""
    return "\n".join(p.name for p in RAIZ.iterdir() if p.is_file()) or "(vazio)"

def calcular(expressao: str) -> str:
    """Avalia uma expressão aritmética simples."""
    if not set(expressao) <= set("0123456789+-*/(). "):
        return "ERRO: só aritmética básica."
    try:
        return str(eval(expressao, {"__builtins__": {}}, {}))
    except Exception as e:
        return f"ERRO: {e}"

FERRAMENTAS = {
    "ler_arquivo":     (ler_arquivo,     "ler_arquivo(caminho) — lê um arquivo de ./dados"),
    "listar_arquivos": (listar_arquivos, "listar_arquivos() — lista os arquivos disponíveis"),
    "calcular":        (calcular,        "calcular(expressao) — avalia uma conta aritmética"),
}
⚠️ A trava do resolve() não é decoração

Sem a checagem is_relative_to, um pedido de ../../.env lê as suas credenciais e o modelo devolve elas no chat. Agente é código que executa entrada não confiável: trate cada ferramenta como um endpoint público.

Passo 3: o laço

Aqui está a decisão de projeto que vale o tutorial inteiro. Existem duas formas de o modelo pedir uma ferramenta:

Para um agente que precisa ser confiável, comece pelo protocolo em texto. Ele não depende de nenhuma capacidade opcional, é trivial de depurar (você lê exatamente o que o modelo escreveu) e migra para tool_calls depois, se você quiser.

SISTEMA = """Você é um agente que resolve tarefas usando ferramentas.

Ferramentas disponíveis:
{ferramentas}

A cada passo responda APENAS um objeto JSON, sem texto em volta, num destes formatos:

  {{"acao": "usar_ferramenta", "ferramenta": "nome", "args": {{...}}, "porque": "1 frase"}}
  {{"acao": "responder", "resposta": "a resposta final ao usuário"}}

Regras:
- Uma ferramenta por vez. Espere o resultado antes de pedir a próxima.
- Se já tem informação suficiente, responda. Não chame ferramenta à toa.
- Se uma ferramenta devolver ERRO, não repita a mesma chamada igual."""


def agente(pergunta: str, modelo="gpub-plus", max_voltas=8, teto_reais=0.50):
    catalogo = "\n".join(f"- {desc}" for _, desc in FERRAMENTAS.values())
    mensagens = [
        {"role": "system", "content": SISTEMA.format(ferramentas=catalogo)},
        {"role": "user", "content": pergunta},
    ]
    gasto = 0.0

    for volta in range(1, max_voltas + 1):
        r = cliente.chat.completions.create(
            model=modelo,
            messages=mensagens,
            temperature=0,                      # agente não improvisa
            response_format={"type": "json_object"},
        )
        bruto = r.choices[0].message.content
        gasto += custo_da_chamada(r)            # ver passo 4
        print(f"  [volta {volta}] R$ {gasto:.4f} — {bruto[:90]}")

        if gasto > teto_reais:
            return f"PARADO: estourou o teto de R$ {teto_reais:.2f}."

        try:
            passo = json.loads(bruto)
        except json.JSONDecodeError:
            mensagens += [{"role": "assistant", "content": bruto},
                          {"role": "user", "content": "Isso não era JSON válido. Responda só o JSON."}]
            continue

        if passo.get("acao") == "responder":
            return passo.get("resposta", "")

        nome = passo.get("ferramenta")
        if nome not in FERRAMENTAS:
            resultado = f"ERRO: ferramenta '{nome}' não existe. Use uma da lista."
        else:
            fn, _ = FERRAMENTAS[nome]
            try:
                resultado = str(fn(**passo.get("args", {})))
            except TypeError as e:
                resultado = f"ERRO de argumentos: {e}"

        mensagens += [
            {"role": "assistant", "content": bruto},
            {"role": "user", "content": f"Resultado de {nome}: {resultado}"},
        ]

    return "PARADO: número máximo de voltas atingido."

Repare no que está ali por um motivo:

Passo 4: saber quanto custou

Toda resposta da API traz o uso de tokens. Como os preços são em reais, o custo de cada volta é uma multiplicação — nada de converter dólar, IOF ou fechamento de câmbio.

PRECOS = {                       # R$ por 1 milhão de tokens
    "gpub-fast": (0.49,  1.09),
    "gpub-mini": (0.59,  3.49),
    "gpub-plus": (0.69,  3.99),
    "gpub-base": (7.90, 17.90),
    "gpub-max":  (17.90, 84.90),
}

def custo_da_chamada(r) -> float:
    entrada, saida = PRECOS[r.model]
    u = r.usage
    return (u.prompt_tokens / 1e6) * entrada + (u.completion_tokens / 1e6) * saida
💰 A conta real de um agente

O custo de um agente cresce em quadrado, não em linha: cada volta reenvia todo o histórico. Um laço de 8 voltas com 2.000 tokens de contexto não custa 8×, custa cerca de 36× uma chamada solta.

Exemplo no gpub-plus: agente de 6 voltas, contexto crescendo até 6 mil tokens, 300 tokens de saída por volta → cerca de R$ 0,02 por execução. Mil execuções por dia = R$ 20/dia. É a conta que decide se o produto fecha.

As três formas de cortar esse custo

Passo 5: rodando

if __name__ == "__main__":
    print(agente("Quantas linhas tem o maior arquivo em ./dados? "
                 "Some esse número com 100 e me diga o total."))
  [volta 1] R$ 0,0004 — {"acao":"usar_ferramenta","ferramenta":"listar_arquivos"...
  [volta 2] R$ 0,0011 — {"acao":"usar_ferramenta","ferramenta":"ler_arquivo","args"...
  [volta 3] R$ 0,0021 — {"acao":"usar_ferramenta","ferramenta":"calcular","args"...
  [volta 4] R$ 0,0029 — {"acao":"responder","resposta":"O maior arquivo tem 412..."}

O maior arquivo tem 412 linhas. Somando 100, o total é 512.

Três centavos de milésimo. E, mais importante, você viu cada passo — o que é a diferença entre depurar um agente e adivinhar por que ele fez o que fez.

De protótipo a produção: o que falta

Memória entre execuções

O agente acima esquece tudo ao terminar. Para memória, persista as mensagens com uma chave de sessão. Não guarde o histórico inteiro para sempre: guarde um resumo gerado pelo modelo mais barato a cada N trocas, e os últimos turnos crus.

def compactar(mensagens, limite=12):
    if len(mensagens) <= limite:
        return mensagens
    antigas, recentes = mensagens[1:-6], mensagens[-6:]
    resumo = cliente.chat.completions.create(
        model="gpub-fast",                       # resumir é tarefa de modelo barato
        messages=[{"role": "user", "content":
                   "Resuma em até 10 linhas os fatos que precisam ser lembrados:\n"
                   + json.dumps(antigas, ensure_ascii=False)}],
    ).choices[0].message.content
    return [mensagens[0], {"role": "user", "content": f"Contexto anterior: {resumo}"}] + recentes

Paralelismo

O laço é sequencial por natureza, mas as tarefas não. Se você tem 500 documentos para o mesmo agente processar, rode 500 laços independentes com um pool de threads. Comece com 8 simultâneos e suba olhando a latência.

Observabilidade

Registre, para cada execução: pergunta, modelo, número de voltas, tokens de entrada e saída, custo, ferramentas chamadas e desfecho. Sem isso você não consegue responder "por que a conta dobrou esta semana?" — e essa pergunta sempre chega.

Quando o agente pede GPU dedicada

A API por token resolve a maioria dos casos e não tem custo parado. Mas há três situações em que vale subir a sua própria instância com o modelo rodando nela:

Para servir o modelo você mesmo, o template vLLM sobe em 1 clique e expõe exatamente a mesma interface compatível com a da OpenAI — o seu agente troca só a base_url e continua funcionando, sem mudar uma linha da lógica.

🧮 Ponto de virada, com números

Agente a R$ 0,02 por execução no gpub-plus. Uma RTX 4090 Econômica a R$ 2,38/h custa R$ 57,12 por dia. A conta empata em torno de 2.800 execuções diárias. Abaixo disso, token é mais barato e não tem máquina ociosa; acima, a GPU dedicada ganha.

Construa o seu agente hoje

API por token compatível com a da OpenAI, cobrada em reais, e GPUs dedicadas quando o volume justificar. Sem assinatura — você paga o que usar.

Criar conta →

Erros que vão te custar tempo

Conclusão

Um agente útil cabe em 120 linhas e não precisa de framework nenhum. O que ele precisa é do que quase nenhum tutorial mostra: teto de custo, recuperação de erro de formato, ferramentas que validam a própria entrada e um registro de para onde o dinheiro foi.

Comece pelo laço em texto puro deste artigo, meça o custo real das suas execuções e só então decida entre continuar por token ou subir a sua própria GPU. A decisão vira aritmética — e é assim que ela deve ser.

Continue: como transformar isso numa plataforma · RAG de produção · Langflow, se você prefere visual