ENPT-BRentrarcadastrar

Criando seu próprio agente

By Vilson Vieira ·

Tags: coding

Introdução

E se eu te disser que o Claude Code pode ser implementado em cerca de 150 linhas de código?!

Claro que existe mais coisa, várias funcionalidades diferentes que foram se acumulando ao longo do desenvolvimento. Mas o núcleo de um agente de programação como Claude Code, Codex, Open Code ou qualquer outro, é bem fácil de implementar. E é isso que vamos construir juntos aqui neste curso curto.

A arquitetura

Antes de tudo, que diabos é um agente?! Mais especificamente um agente de programação.

Um agente é basicamente um LLM com acesso a ferramentas, rodando em loop:

graph TD
  A[Agent] --> B[LLM]
  A --> C[Tools]

O agente envia mensagens para o LLM e ele responde de volta. Com um detalhe particular que faz tudo realmente explodir em possibilidades: o LLM pode pedir para o agente executar ferramentas!

Mas por que isso é tão importante?! Lembre-se de que LLMs são modelos "estáticos". Eles foram treinados há alguns meses com o estado da Internet daquela época. Então tudo que um LLM sabe está congelado no tempo: ele não tem acesso ao mundo atual, não tem acesso ao seu computador, não tem acesso a chamar APIs para trazer mais sinais do mundo externo.

É por isso que damos ferramentas aos LLMs! É uma forma bem engenhosa de permitir que eles capturem sinais do mundo atual e atuem nesse mundo. Com ferramentas, eles podem ler quaisquer arquivos no seu computador, fazer requisições para qualquer API na Internet ou executar qualquer programa disponível na sua máquina.

Ok, ferramentas são bem legais, mas como isso funciona? Bem, o loop é bem direto:

Fica bem mais fácil pensar nisso com um exemplo prático.

Vamos dizer que você quer saber como está o clima em Londres hoje (você poderia só ter o agente retornando "Nublado." e acertar 80% das vezes, mas vamos realmente usar um LLM para isso :D).

Lembre-se de que o LLM não sabe nada sobre o clima de hoje em nenhum lugar do mundo. Então ele vai precisar de acesso a alguma ferramenta que dê acesso a esse sinal externo! Nós podemos ver como isso acontece nesta sequência:

sequenceDiagram
    Agent->>+LLM: What's the temperature?
    LLM->>+Agent: <toolcall>get_weather(london)</toolcall>
    Agent->>+ToolRegistry: get_weather(london)
    ToolRegistry->>Agent: {"temp": 9, "unit": "Celcius", "weather": "Cloudy"}
    Agent->>+LLM: Tool result: 9 deg
    LLM->>+Agent: The temperature in London is 9 degrees Celcius.

Aqui estamos usando uma ferramenta bem simples como exemplo, mas poderia ser literalmente qualquer procedimento! E agora você deve estar se perguntando: Ok, mas como o LLM sabe sobre a ferramenta get_weather()?!.

Bem, eu omiti um passo importante! Antes de enviar qualquer mensagem para o LLM, devemos "prepará-lo" para dar certo. Fazemos isso enviando uma mensagem inicial dizendo tudo que o LLM deve saber antes de começar. Chamamos isso de system prompt. É só um nome chique para uma mensagem inicial.

No system prompt teremos algo como:

You are an weather assistant.
You should provide the current temperature in a single sentence.

You have access to these tools:

get_weather(location): Returns the current temperature and weather in this JSON schema:
    {"temp": float, "unit": string, "weather": string}

Example:

get_weather(london): {"temp": 10, "unit": "Celcius", "weather": "Sun with clouds"}

Important: when answering, use the temperature unit requested by the user, if no unit is
requested, default to Celcius.

Note que também estamos definindo uma "persona" para o LLM (e para o agente): a partir de agora ele será um assistente de clima, nada além disso. E ele terá acesso a uma ferramenta que retorna o clima em um formato JSON.

Mas nós ainda não implementamos a ferramenta! Sim, isso mesmo, por enquanto só precisamos definir do que se trata a ferramenta e o formato de suas entradas e saídas. A implementação será definida no agente depois e é totalmente independente do LLM!

Certo, acredito que já chega de teoria, vamos colocar isso em prática para criar nosso próprio agente!

Setup

Vamos usar Python para programar o agente, mas você pode usar qualquer linguagem de programação que quiser. Se você nunca usou Python antes, por favor veja nosso curso Como começar a programar. E se você nunca usou um terminal baseado em Unix antes, por favor veja nosso curso Como usar terminais.

Antes de tudo, crie uma pasta para hospedar seu projeto, um ambiente Python e instale o SDK da OpenAI:

mkdir ai-agent
cd ai-agent
python -m venv .venv
source .venv/bin/activate
pip install openai

Eu recomendo que você crie uma conta no OpenRouter. Você pode usar os LLMs da OpenAI diretamente, mas o OpenRouter permite alternar entre todo um ecossistema de LLMs, incluindo os de código aberto! Basta ir para o OpenRouter, adicionar uns $10 em créditos, criar uma chave de API e copiar e colar aqui:

export OPENROUTER_API_KEY=cole a API aqui

Depois que isso estiver configurado, estamos prontos para criar nosso bebê!

Passo 1: O LLM

Primeiro vamos testar nossa conexão com o OpenRouter através do SDK da OpenAI. Essa é a biblioteca que vamos usar para enviar mensagens ao LLM.

Crie um arquivo chamado agent.py com o seguinte:

import os
from openai import OpenAI

MODEL = "anthropic/claude-opus-4-6"

client = OpenAI(
    base_url="https://openrouter.ai/api/v1",
    api_key=os.environ["OPENROUTER_API_KEY"],
)

Aqui basicamente estamos criando uma instância de OpenAI e dizendo que queremos usar o LLM Claude Opus 4.6 da Anthropic. Também estamos passando a chave de API do OpenRouter que você acabou de criar.

messages = [
  {"role": "user", "content": "What's your name?"},
]

response = client.chat.completions.create(
    model=MODEL,
    messages=messages,
)

message = response.choices[0].message
print(message)

E então enviamos uma mensagem para o LLM, perguntando o nome dele. Você deve ver algo como o seguinte na resposta:

ChatCompletionMessage(
  content="My name is Claude. I'm an AI assistant made by Anthropic. How can I help you today?",
  refusal=None,
  role='assistant',
  annotations=None,
  audio=None,
  function_call=None,
  tool_calls=None,
  reasoning=None
)

Ótimo! Então temos uma forma de conversar com o LLM. Estamos na metade do caminho, só precisamos adicionar as ferramentas!

Passo 2: As ferramentas

Você provavelmente está pensando: Claude Code e outros agentes provavelmente precisam de toneladas de ferramentas para fazer bom uso do mundo externo, como o computador em que o agente está rodando ou a Internet.

Bom, sim e não :-) Isso realmente depende de como esses modelos LLM foram treinados e pós-treinados. O que sabemos hoje é que eles foram bastante pós-treinados em leitura, escrita e edição de código, como esperado. Mas a ferramenta que abre o mundo para eles é a ferramenta bash! E os LLMs de programação dominam bem seu uso!

Com a ferramenta bash você basicamente pode deixar o agente executar qualquer programa! Você quer acesso à Internet? Basta rodar o comando curl. Quer checar o tamanho do disco? Basta rodar df. Quer verificar quais processos estão em execução? Rode ps.

Agentes e sistemas baseados em Unix, onde tudo é um arquivo, formam uma combinação poderosa. LLMs entendem texto muito bem, então não é surpresa que consigam trabalhar muito bem com programas CLI em uma máquina Unix que basicamente recebe e retorna texto.

E se você ainda não confia em mim de que isso é suficiente, existem harnesses de agente como o Pi que usam apenas essas 4 ferramentas que estamos criando aqui, nada além disso! Eu uso o Pi diariamente (bem, usei ele para criar este webapp que você está usando :-)) e não sinto necessidade de usar o Claude Code.

Certo, então vamos implementar essas ferramentas! Primeiro, precisamos definir do que elas se tratam: seu nome, descrição e parâmetros:

tools = [
    {
        "type": "function",
        "function": {
            "name": "read",
            "description": "Read a UTF-8 text file",
            "parameters": {
                "type": "object",
                "properties": {
                    "path": {"type": "string"}
                },
                "required": ["path"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "write",
            "description": "Overwrite a UTF-8 text file",
            "parameters": {
                "type": "object",
                "properties": {
                    "path": {"type": "string"},
                    "content": {"type": "string"},
                },
                "required": ["path", "content"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "edit",
            "description": "Replace exact text in a file",
            "parameters": {
                "type": "object",
                "properties": {
                    "path": {"type": "string"},
                    "old_text": {"type": "string"},
                    "new_text": {"type": "string"},
                },
                "required": ["path", "old_text", "new_text"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "bash",
            "description": "Run a shell command in the current directory",
            "parameters": {
                "type": "object",
                "properties": {
                    "command": {"type": "string"}
                },
                "required": ["command"],
            },
        },
    },
]

Depois de definir as assinaturas e descrições das ferramentas, vamos finalmente implementá-las como funções Python simples:

from pathlib import Path
import subprocess


def tool_read(path: str) -> str:
    return Path(path).read_text(encoding="utf-8")


def tool_write(path: str, content: str) -> str:
    p = Path(path)
    p.parent.mkdir(parents=True, exist_ok=True)
    p.write_text(content, encoding="utf-8")
    return f"wrote {path}"


def tool_edit(path: str, old_text: str, new_text: str) -> str:
    p = Path(path)
    original = p.read_text(encoding="utf-8")
    if old_text not in original:
        return "edit failed: old_text not found"
    updated = original.replace(old_text, new_text, 1)
    p.write_text(updated, encoding="utf-8")
    return f"edited {path}"


def tool_bash(command: str) -> str:
    result = subprocess.run(
        command,
        shell=True,
        text=True,
        capture_output=True,
        timeout=30,
    )
    output = (result.stdout or "") + (result.stderr or "")
    return output[:8000] or "(no output)"


def execute_tool(name: str, args: dict) -> str:
    try:
        if name == "read":
            return tool_read(args["path"])
        if name == "write":
            return tool_write(args["path"], args["content"])
        if name == "edit":
            return tool_edit(args["path"], args["old_text"], args["new_text"])
        if name == "bash":
            return tool_bash(args["command"])
        return f"unknown tool: {name}"
    except Exception as e:
        return f"tool error: {e}"

Note que também adicionamos uma função chamada execute_tool() para mapear o nome de uma ferramenta para sua implementação e então executá-la e retornar a saída.

Passo 3: O loop do agente

Agora temos que colocar essas ferramentas em uso, executando-as quando o LLM pedir para nós fazer isso:

import json

SYSTEM_PROMPT = (
    "You are a precise coding assistant. "
    "Use tools when needed. Keep changes minimal and explain clearly."
)


def run_agent(user_request: str):
    messages = [
        {"role": "system", "content": SYSTEM_PROMPT},
        {"role": "user", "content": user_request},
    ]

    while True:
        response = client.chat.completions.create(
            model=MODEL,
            messages=messages,
            tools=tools,
        )

        message = response.choices[0].message
        messages.append(message)

        if not message.tool_calls:
            print(f"\nassistant> {message.content}\n")
            return

        for tool_call in message.tool_calls:
            name = tool_call.function.name
            args = json.loads(tool_call.function.arguments or "{}")
            result = execute_tool(name, args)
            print(f"tool> {name}({args})\n\t{result}")
            messages.append(
                {
                    "role": "tool",
                    "tool_call_id": tool_call.id,
                    "content": result,
                }
            )

Vamos olhar de perto o que está acontecendo aqui porque na verdade existem dois loops rodando.

Primeiro enviamos um system prompt, instruindo o LLM a se comportar como um assistente de programação e a usar as ferramentas que definimos.

O resto do código você já conhece, mas note uma mudança importante: quando recebemos a resposta do LLM, verificamos message.tool_calls. Vai haver chamadas de ferramenta se o LLM "sentiu" necessidade de executá-las para fornecer uma resposta melhor ao usuário!

Então, iteramos em um segundo loop: para cada chamada de ferramenta executamos a função que corresponde ao nome da ferramenta, pegamos o resultado e o adicionamos às mensagens que já enviamos. Note que definimos role como tool, indicando ao LLM que este é o resultado daquela ferramenta que ele pediu antes.

Então repetimos o passo do loop while-true novamente, agora com os resultados das ferramentas anexados às mensagens. Desta vez o LLM vai usar os resultados das ferramentas para melhorar seu "raciocínio" e finalmente responder ao usuário com mais contexto sobre a pergunta.

Se mais ferramentas forem necessárias, ele pedirá mais chamadas. Caso contrário, ele só mostrará a resposta ao usuário e encerrará o loop.

Passo 4: O loop de chat

Desculpa, menti de novo, na verdade existe mais um loop final :-) O loop mais externo é o que transforma isso em um chat de verdade como você vê no Claude Code e em outros agentes.

É bem simples: esperamos uma entrada do usuário e enviamos para o agente. E repetimos isso até o usuário sair:

if __name__ == "__main__":
    # ANSI colors: cyan prompt, reset after text
    CYAN = "\033[96m"
    RESET = "\033[0m"

    print("My Coding Agent. Type 'exit' to quit.\n")

    while True:
        try:
            user_text = input(f"{CYAN}agent>{RESET} ").strip()
        except (EOFError, KeyboardInterrupt):
            print("\nbye")
            break

        if not user_text:
            continue
        if user_text.lower() in {"exit", "quit"}:
            print("bye")
            break

        run_agent(user_text)

Executando

É só isso?! Sim, é isso! Parabéns! Você construiu seu próprio Claude Code!

Para rodar, só lembre de definir a chave de API do OpenRouter e chamar o interpretador Python:

export OPENROUTER_API_KEY="your-key"
python agent.py

Você deve ver um prompt como este. Pergunte o que quiser e veja se ele consegue usar as ferramentas como esperado:

E agora?!

Você pode ver a implementação completa aqui. Menos de 170 linhas de código! Baixe, adapte para suas próprias necessidades, traduza para sua linguagem de programação favorita.

O ponto importante aqui é que agora você sabe como o Claude Code e outros agentes são implementados.

Ainda falta uma peça grande, porém: o modelo! Mas isso fica para outra hora, em outro curso! Continue acompanhando e crie sua conta para ter acesso quando ele for publicado!