O que é um agente de IA?
Antes de escrever uma linha de código, vale separar dois conceitos que vivem misturados:
- LLM (Large Language Model) — uma função pura: recebe texto, devolve texto. Sem memória, sem ações, sem mundo externo. Você manda uma mensagem, ele responde uma vez e acabou.
- Agente de IA — um LLM dentro de um loop, com acesso a ferramentas que ele decide quando chamar. O loop só termina quando o modelo diz “acabei”.
A diferença não é técnica, é arquitetural. Um chatbot é um LLM com histórico. Um agente é um LLM com autonomia para agir.
Os quatro componentes de um agente
Praticamente todo agente — do mais simples ao mais sofisticado — combina os mesmos quatro elementos:
- Modelo — o “cérebro” que decide o próximo passo. Hoje, normalmente um LLM (Claude, GPT, Llama).
- Ferramentas (tools) — funções do seu código que o modelo pode chamar: ler arquivo, consultar banco, fazer requisição HTTP, executar SQL.
- Loop de execução — quem orquestra: pergunta ao modelo, executa a ferramenta que ele pediu, devolve o resultado, repete.
- Critério de parada — quando o modelo sinaliza
end_turn, ou quando o loop atinge um limite de iterações.
É isso. Tudo o que você ouve falar sobre “agentes autônomos”, “multi-agentes”, “ReAct”, “function calling” — são variações em cima dessa estrutura.
A anatomia de uma chamada com tools
A API da Anthropic (e a da OpenAI, com nomes diferentes) expõe esse padrão de forma direta. O fluxo é:
Você → [mensagens + descrição das tools] → Modelo
Modelo → responde tool_use(name, input) → Você
Você executa a tool → manda tool_result → Modelo
Modelo → responde texto final (end_turn)
O modelo nunca executa nada — ele só descreve o que quer fazer. Quem executa é o seu código. Isso é fundamental: o agente é seu, o LLM é só o tomador de decisão.
Construindo um agente mínimo em Ruby puro
Vamos construir um agente que sabe duas coisas:
current_time— retorna a hora atual.read_file— lê um arquivo do disco.
Sem gem anthropic, sem Rails. Para a chamada HTTP, vamos usar a HTTParty — uma gem clássica do ecossistema Ruby que deixa o cliente bem mais enxuto que net/http puro.
gem install httparty
Estrutura
mini_agent/
├── lib/
│ ├── agent.rb # o loop
│ ├── client.rb # cliente HTTP da API
│ └── tools.rb # implementação das ferramentas
└── bin/agent # executável
O cliente HTTP
# lib/client.rb
require "httparty"
class Client
include HTTParty
base_uri "https://api.anthropic.com"
format :json
MODEL = "claude-sonnet-4-6"
def initialize(api_key: ENV.fetch("ANTHROPIC_API_KEY"))
@headers = {
"x-api-key" => api_key,
"anthropic-version" => "2023-06-01",
"content-type" => "application/json"
}
end
def call(messages, tools)
body = {
model: MODEL,
max_tokens: 1024,
tools: tools,
messages: messages
}
self.class.post("/v1/messages", headers: @headers, body: body.to_json).parsed_response
end
end
A HTTParty resolve três chatices de uma vez: parseia o JSON da resposta automaticamente (parsed_response), aceita base_uri para deixar as rotas curtas, e expõe post/get/put como métodos de classe. O cliente inteiro cabe em pouco mais de 20 linhas.
As ferramentas
# lib/tools.rb
module Tools
DEFINITIONS = [
{
name: "current_time",
description: "Retorna a data e hora atual no fuso de São Paulo.",
input_schema: { type: "object", properties: {} }
},
{
name: "read_file",
description: "Lê o conteúdo de um arquivo de texto local.",
input_schema: {
type: "object",
properties: {
path: { type: "string", description: "Caminho do arquivo" }
},
required: ["path"]
}
}
].freeze
module_function
def run(name, input)
case name
when "current_time" then Time.now.strftime("%Y-%m-%d %H:%M:%S %z")
when "read_file" then File.read(input.fetch("path"))
else "tool desconhecida: #{name}"
end
rescue => e
"erro: #{e.message}"
end
end
A DEFINITIONS é o que o modelo enxerga — ele lê o description para decidir quando usar cada tool. O run é o despachante: recebe o nome e os argumentos que o modelo pediu, executa, devolve uma string.
O rescue é proposital: se a tool falhar, o erro vira input para o modelo, que normalmente consegue se recuperar (tentar outro caminho, pedir mais informação, desistir educadamente).
O loop
O coração do agente. Com comentários explicando cada passo:
# lib/agent.rb
require_relative "client"
require_relative "tools"
class Agent
MAX_ITERATIONS = 10
def initialize
@client = Client.new
@messages = []
end
def ask(prompt)
@messages << { role: "user", content: prompt }
MAX_ITERATIONS.times do
response = @client.call(@messages, Tools::DEFINITIONS)
@messages << { role: "assistant", content: response["content"] }
return final_text(response) if response["stop_reason"] == "end_turn"
tool_results = response["content"]
.select { |block| block["type"] == "tool_use" }
.map { |block| execute(block) }
@messages << { role: "user", content: tool_results }
end
"[limite de iterações atingido]"
end
private
def execute(block)
output = Tools.run(block["name"], block["input"])
{
type: "tool_result",
tool_use_id: block["id"],
content: output.to_s
}
end
def final_text(response)
response["content"]
.select { |block| block["type"] == "text" }
.map { |block| block["text"] }
.join("\n")
end
end
Três coisas merecem atenção aqui:
- O histórico inteiro vai junto a cada chamada. O LLM não tem memória; quem mantém o estado é o array
@messages. tool_use_idprecisa bater. Cadatool_useque o modelo emite tem um ID único, e otool_resultcorrespondente precisa referenciar esse mesmo ID — é assim que o modelo associa pergunta e resposta.MAX_ITERATIONSé um cinto de segurança. Sem ele, um modelo confuso pode te custar caro. Sempre tenha um teto.
O executável
#!/usr/bin/env ruby
# bin/agent
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
require "agent"
agent = Agent.new
puts agent.ask(ARGV.join(" "))
chmod +x bin/agent
export ANTHROPIC_API_KEY="sk-ant-..."
bin/agent "Que horas são agora? E me diga o que tem no Gemfile."
Saída esperada (algo nessa linha):
Agora são 2026-05-11 14:23:07 -0300.
O Gemfile contém as dependências do projeto: jekyll na versão 4.4.1,
o tema minima, e plugins como jekyll-feed e jekyll-seo-tag.
Por baixo dos panos, o que aconteceu:
- O modelo recebeu a pergunta e decidiu chamar
current_time. - Você executou e devolveu o resultado.
- O modelo decidiu chamar
read_filecompath: "Gemfile". - Você leu o arquivo e devolveu o conteúdo.
- O modelo escreveu a resposta final em texto e marcou
end_turn.
Tudo isso em uma única chamada agent.ask.
O que esse exemplo deixa de fora (de propósito)
Para manter o foco, o agente acima ignora algumas coisas que você vai querer em produção:
- Streaming — a resposta vem inteira, em vez de chegar token a token.
- Persistência de conversa — o histórico mora em memória e morre com o processo.
- Validação de input das tools — o modelo pode mandar argumentos malformados; aqui só confiamos.
- Custo e tokens — não há contagem nem limite de gasto.
- Permissões — o
read_filelê qualquer arquivo do sistema. Em um agente real, você precisa de um sandbox.
Cada um desses itens vira um post separado. O ponto aqui é mostrar que um agente de IA, em sua forma mais essencial, cabe em umas 60 linhas de Ruby.
Resumo
| Componente | Responsabilidade | No exemplo |
|---|---|---|
| Modelo | Decidir o próximo passo | Claude via API HTTP |
| Tools | Executar ações no mundo | current_time, read_file |
| Loop | Orquestrar pergunta → ação → resultado | Agent#ask |
| Parada | Saber quando terminar | stop_reason == "end_turn" + teto de iterações |
Agente de IA não é mágica — é um padrão arquitetural simples em volta de um LLM. Uma vez que o loop está claro na sua cabeça, o resto (memória de longo prazo, multi-agentes, MCP, RAG) são camadas que você adiciona conforme o problema pede. Mas tudo começa aqui: um modelo, algumas ferramentas, um loop.