"Agente de IA" é um dos termos mais sobrecarregados do vocabulário de engenharia atual. Dependendo de quem usa, pode significar desde uma única chamada de LLM com tools disponíveis até um sistema com dezenas de modelos colaborando em tempo real. Para tomar decisões de arquitetura informadas, é necessário vocabulário preciso: o que distingue um tipo de agente do outro, quando cada arquitetura faz sentido, e quais são os trade-offs reais de cada escolha.
A definição funcional de agente de IA é: qualquer sistema onde um LLM toma decisões iterativas usando ferramentas e feedback do ambiente para alcançar um objetivo. O que distingue um agente de uma chamada de LLM simples é o loop — o modelo não responde uma vez e termina, mas age, observa o resultado, e decide o próximo passo com base nessa observação. Esse loop é o coração de qualquer arquitetura de agente.
A anatomia do loop de agência
O loop de agência segue o padrão perceive → reason → act, repetido até que a condição de parada seja atingida:
Perceive — o agente recebe o estado atual: a tarefa original, resultados de ações anteriores, e contexto acumulado. Tecnicamente, isso é a lista de mensagens (messages) crescendo a cada ciclo.
Reason — o LLM processa o estado atual e gera um raciocínio + decisão de próxima ação. Pode ser uma tool call, uma resposta final, ou (em agentes planejadores) um passo de raciocínio explícito antes de qualquer ação.
Act — a ação é executada: tool call, escrita de arquivo, chamada de API. O resultado é injetado de volta no contexto como mensagem do usuário, e o ciclo recomeça.
O loop de agência não é abstração — é uma lista de mensagens que cresce a cada ciclo. Cada turno adiciona: a resposta do assistente (com tool calls) e os resultados das tools (como mensagem do usuário). Quanto mais ciclos, maior o contexto; quanto maior o contexto, maior o custo e o risco de degradação do raciocínio. Arquitetura de agente é, em grande parte, gestão de contexto: o que entra, o que fica, o que é descartado.
Taxonomia prática: cinco arquiteturas
1. Agente reativo
O agente reativo é o mais simples: recebe uma tarefa, usa tools para completá-la em uma sessão, e não mantém estado entre sessões. Cada execução começa do zero. O histórico de mensagens existe durante a execução, mas é descartado quando a tarefa termina.
Casos de uso adequados: geração de código para uma especificação bem definida, resposta a perguntas sobre um repositório, refatoração de um arquivo com regras explícitas, análise de um diff. Qualquer tarefa autocontida que não precise de contexto de sessões anteriores.
Vantagens: menor overhead — latência previsível, custo por token controlado, comportamento determinístico (para a mesma entrada, o mesmo processo). Limitação: a ausência de aprendizado entre sessões. Cada execução é independente, e o agente não se beneficia de interações anteriores.
2. Agente com memória
O agente com memória mantém alguma forma de estado persistente entre sessões. Existem três mecanismos principais com características distintas:
Scratchpad textual — arquivo de texto que o agente lê no início de cada sessão e escreve ao final. Simples de implementar, sem infraestrutura adicional. Limitado por tamanho e falta de busca eficiente. Ideal para contexto pequeno e estruturado.
Banco de dados vetorial — embeddings de interações e decisões anteriores são armazenados e recuperados por similaridade semântica. Escala para grandes volumes de histórico, mas adiciona complexidade de infraestrutura (vector DB, pipeline de embedding) e custo adicional.
Armazenamento estruturado — banco de dados relacional ou chave-valor com esquema explícito. Ideal para contexto tipado: convenções de código, decisões de arquitetura, estado de projetos em andamento. Requer mais código para gerenciar, mas oferece consultas precisas e sem ambiguidade semântica.
O sistema de memória do Claude Code — arquivos Markdown em .claude/memory/ lidos no início de cada sessão — é um exemplo de scratchpad estruturado. A memória define o contexto persistente; a execução da tarefa consome e pode enriquecer esse contexto.
3. Agente planejador
O agente planejador decompõe o objetivo em subtarefas antes de executar. Em vez de agir imediatamente, ele primeiro raciocina sobre o plano e só então começa a usar tools. O raciocínio explícito antes da ação é o que define essa arquitetura.
O padrão mais influente para agentes planejadores é o ReAct (Reasoning + Acting), de Yao et al. (2022): o modelo alterna entre passos de raciocínio explícito ("preciso ler o arquivo X para entender Y antes de modificar Z") e passos de ação. O raciocínio é gerado como texto visível — não é um processo interno opaco, mas um output estruturado que pode ser verificado.
A diferença entre agente reativo e planejador não é binária — é uma questão de system prompt e de como o planejamento é enforced. Um agente reativo com instrução "antes de usar qualquer ferramenta, escreva um plano detalhado" se comporta como um agente planejador. A distinção arquitetural robusta é quando esse planejamento é enforced por mecanismo — paradas obrigatórias, verificação de artefatos — não apenas por instrução. Esse é o tema central do próximo conceito.
Um agente que age sem planejar é como um desenvolvedor que começa a digitar código sem ler o ticket. Funciona para tarefas triviais; falha sistematicamente em tarefas com dependências não óbvias. O benefício do planejamento explícito não é só qualidade do output — é rastreabilidade: quando o plano está escrito, o humano pode verificar e corrigir antes que o agente execute ações difíceis de reverter. Planejamento é o mecanismo primário de controle em agentes autônomos.
4. Agente orquestrador
O agente orquestrador não executa tarefas diretamente — ele coordena outros agentes. Suas "ações" são: delegar subtarefas para agentes especializados, agregar resultados, decidir o próximo passo com base nos outputs recebidos, e gerenciar o estado global do pipeline.
O orquestrador resolve o problema de complexidade que emerge em tarefas longas: um único agente acumulando contexto excessivo perde foco e degrada a qualidade das decisões progressivamente. Ao dividir a tarefa em subtarefas e distribuir para agentes especializados com contextos menores e bem definidos, o orquestrador mantém cada agente focado em uma responsabilidade clara com um window de contexto gerenciável.
O custo é real: o orquestrador introduz latência (chamadas sequenciais de agentes), custo multiplicado (cada sub-agente é uma sessão de LLM separada), e o risco crítico de erro em cascata — se um sub-agente produz output incorreto e o orquestrador não verifica explicitamente, esse erro se propaga para todas as etapas subsequentes sem nenhum agente individual ter "falhado" pelo seu próprio critério.
5. Multi-agent systems
Sistemas multi-agente têm múltiplos agentes com papéis distintos colaborando via troca de mensagens. Diferente do orquestrador (que tem hierarquia clara e controle centralizado), sistemas multi-agente podem ter topologias variadas: hierarquia, par-a-par, ou especialização paralela.
Exemplos concretos: um agente de pesquisa que identifica perguntas abertas, um agente de implementação que responde cada pergunta com código, um agente de síntese que integra os resultados — três agentes com interações definidas. Ou múltiplos agentes trabalhando em paralelo em subproblemas independentes, com um agente de integração que agrega os resultados.
A complexidade de sistemas multi-agente é substancial: orquestração das interações, gerenciamento do estado global compartilhado, tratamento de falhas parciais (o que fazer quando um de cinco agentes falha?), e debugging de comportamentos emergentes que não eram observáveis em nenhum agente individual. São adequados para problemas que genuinamente não cabem em um único agente — não como ponto de partida.
Sistemas multi-agente são a solução certa para um conjunto pequeno de problemas. Para a maioria das tarefas de engenharia, um agente planejador bem configurado supera um sistema multi-agente mal arquitetado — e é dramaticamente mais fácil de debugar. A regra: use a arquitetura mais simples que resolve o problema. Adicione complexidade apenas quando há evidência de que a solução mais simples não é suficiente, não por expectativa de que mais sofisticação implique melhor resultado.
Skills no Claude Code
Skills são comandos slash customizados que encapsulam workflows reutilizáveis. Quando invocada com /nome-da-skill, o Claude Code carrega um arquivo Markdown de definição de skill e executa o workflow nele descrito — essencialmente um agente planejador especializado com propósito e processo fixos.
Uma skill é um arquivo Markdown em .claude/skills/ (por projeto) ou ~/.claude/skills/ (global). A estrutura mínima define: contexto da skill (o que ela faz e quando usar), o processo a ser seguido (a sequência de passos), e o formato do output esperado. O agente interpreta esse arquivo como instrução de workflow a cada invocação.
Skills úteis em times de engenharia:
/spec — dado uma descrição de feature em linguagem natural, gera um documento de especificação estruturado: objetivo, comportamento esperado, edge cases, não-objetivos, critérios de aceite.
/review — dado um diff ou arquivo modificado, executa code review com critérios predefinidos: segurança, performance, legibilidade, cobertura de casos de erro, aderência às convenções do projeto.
/adr — dado um problema de design, gera um Architecture Decision Record no formato do time: contexto, opções consideradas, decisão, consequências.
/incident — dado um erro em produção, conduz análise de root cause com template específico: timeline, impacto, causa raiz, ações imediatas, ações preventivas.
A distinção entre skills e MCP servers é importante: uma skill define um processo de interação (sequência de passos que o agente executa); um MCP server define capacidades (ferramentas que o agente pode usar). São complementares — uma skill /review pode usar um MCP server de GitHub para buscar dados da PR, e usar outro MCP server para postar o comentário de revisão.
A distinção entre skills e código direto: skills são interpretadas pelo LLM a cada execução — flexíveis, mas com variabilidade de output. Código direto é determinístico e verificável. Para operações críticas ou de alta frequência, prefira código. Para workflows exploratórios ou que requerem julgamento contextual variável, skills são mais adequadas.
Escolhendo a arquitetura certa
A decisão segue uma hierarquia de complexidade crescente. Parta sempre do mais simples e adicione complexidade apenas quando há justificativa concreta:
A tarefa é autocontida e termina em uma sessão? → Agente reativo. Sem necessidade de persistência entre execuções, sem estado compartilhado, sem coordenação.
Precisa de consistência entre sessões? → Adicione memória. Escolha o mecanismo pelo volume e estrutura do contexto: scratchpad para pouco contexto estruturado, vector DB para muito histórico não estruturado, banco de dados para contexto tipado e consultável.
A tarefa tem dependências não óbvias ou risco de ações prematuras? → Adicione planejamento explícito. O plano deve ser um artefato verificável, não apenas texto descartável antes da execução.
O contexto necessário para completar a tarefa excede o que um único agente pode gerenciar sem degradar? → Considere orquestrador. Divida por responsabilidade, não por conveniência.
Há subtarefas genuinamente independentes que se beneficiariam de paralelismo ou de contextos completamente isolados? → Multi-agent. E documente explicitamente por que as abordagens anteriores não eram suficientes.
Trade-offs de arquitetura
Latência cresce com a complexidade: um agente reativo tem latência proporcional ao número de ciclos de tool use; um orquestrador com quatro sub-agentes sequenciais tem latência que é a soma de quatro sessões de LLM mais o tempo de orquestração. Sistemas multi-agente paralelos podem ser mais rápidos que sequenciais, mas ainda têm overhead de coordenação.
Custo é multiplicativo em arquiteturas complexas. Cada sub-agente consome tokens independentemente — o contexto não é compartilhado. Um orquestrador que passa o output completo de um sub-agente para o próximo duplica o custo de tokens desse output (aparece tanto como output do produtor quanto como input do consumidor).
Debuggability é a dimensão mais subestimada. Um agente reativo pode ser traced linearmente: mensagem 1, tool call, resultado, mensagem 2, resposta final. Um orquestrador com quatro sub-agentes tem quatro históricos de conversa separados, cada um com seu próprio contexto e raciocínio. Quando o resultado final está errado, identificar qual sub-agente produziu o output incorreto que causou a falha cascateada requer instrumentação explícita.
Erro em cascata é o risco diferencial de arquiteturas com múltiplos agentes. Em um sistema sequencial, um erro em qualquer fase contamina todas as fases subsequentes. Um agente de pesquisa que alucina um fato passa essa alucinação para o agente de planejamento, que a incorpora no spec, que o agente de implementação usa como verdade. Nenhum agente "falhou" pelo seu próprio critério — mas o sistema produziu output incorreto. Mitigação requer verificações explícitas entre fases, não apenas confiança no modelo.
Comparação por linguagem
// PlanningAgent.cs — Loop ReAct: planeja → age → observa → repete
using Anthropic.SDK;
using Anthropic.SDK.Messaging;
using System.Text.Json;
public class PlanningAgent(AnthropicClient client)
{
private const string SystemPrompt = """
Antes de usar qualquer ferramenta, escreva:
PLANO: [lista numerada dos passos que você vai executar]
Só então execute os passos na ordem planejada.
Se o objetivo ficar ambíguo durante a execução, reavalie o plano antes de continuar.
""";
private static readonly List<Tool> Tools =
[
new()
{
Name = "read_file",
Description = "Lê o conteúdo de um arquivo no projeto",
InputSchema = new InputSchema
{
Type = "object",
Properties = new Dictionary<string, Property>
{
["path"] = new() { Type = "string", Description = "Caminho relativo ao projeto" }
},
Required = ["path"]
}
},
new()
{
Name = "list_files",
Description = "Lista arquivos em um diretório",
InputSchema = new InputSchema
{
Type = "object",
Properties = new Dictionary<string, Property>
{
["dir"] = new() { Type = "string", Description = "Diretório a listar" }
},
Required = ["dir"]
}
}
];
public async Task<string> RunAsync(string goal, CancellationToken ct = default)
{
var messages = new List<Message>
{
new() { Role = RoleType.User, Content = [new TextContent { Text = goal }] }
};
// Loop ReAct: continua até stop_reason = "end_turn"
while (true)
{
var response = await client.Messages.GetClaudeMessageAsync(new()
{
Model = "claude-opus-4-7",
MaxTokens = 8192,
System = SystemPrompt,
Messages = messages,
Tools = Tools,
}, ct);
// Registra resposta do assistente no histórico
messages.Add(new() { Role = RoleType.Assistant, Content = response.Content });
if (response.StopReason == "end_turn")
return string.Join("\n", response.Content.OfType<TextContent>().Select(t => t.Text));
// Executa cada tool solicitada e injeta resultados no próximo turno
var toolResults = new List<IMessageContent>();
foreach (var block in response.Content.OfType<ToolUseContent>())
{
var result = await RunToolAsync(block.Name, block.Input, ct);
toolResults.Add(new ToolResultContent { ToolUseId = block.Id, Content = result });
}
messages.Add(new() { Role = RoleType.User, Content = toolResults });
}
}
private static async Task<string> RunToolAsync(
string name, JsonElement input, CancellationToken ct) => name switch
{
"read_file" => await File.ReadAllTextAsync(
input.GetProperty("path").GetString()!, ct),
"list_files" => string.Join("\n", Directory.GetFiles(
input.GetProperty("dir").GetString()!, "*", SearchOption.AllDirectories)),
_ => $"Tool desconhecida: {name}"
};
}
Implementação de um agente planejador em C# com o loop ReAct completo: acumulação de mensagens, execução de tools, injeção de resultados, e condição de parada. O system prompt força planejamento explícito antes de qualquer ação.
# agents.py — Agente reativo vs. agente com memória persistente
import json
from pathlib import Path
from dataclasses import dataclass, field
import anthropic
client = anthropic.Anthropic()
TOOLS = [{
"name": "read_file",
"description": "Lê o conteúdo de um arquivo",
"input_schema": {
"type": "object",
"properties": {"path": {"type": "string"}},
"required": ["path"]
}
}]
def _run_tool(name: str, inp: dict) -> str:
if name == "read_file":
p = Path(inp["path"])
return p.read_text(encoding="utf-8") if p.exists() else f"Não encontrado: {p}"
return f"Tool desconhecida: {name}"
def _agent_loop(system: str, task: str) -> str:
messages = [{"role": "user", "content": task}]
while True:
r = client.messages.create(
model="claude-opus-4-7", max_tokens=8192,
system=system, messages=messages, tools=TOOLS,
)
messages.append({"role": "assistant", "content": r.content})
if r.stop_reason == "end_turn":
return next(b.text for b in r.content if b.type == "text")
results = [
{"type": "tool_result", "tool_use_id": b.id, "content": _run_tool(b.name, b.input)}
for b in r.content if b.type == "tool_use"
]
messages.append({"role": "user", "content": results})
# --- Agente Reativo: sem estado entre chamadas ---
def reactive_agent(task: str) -> str:
"""Cada chamada é independente. Nenhum contexto persiste."""
return _agent_loop(
"Você é um assistente de engenharia. Complete a tarefa usando as ferramentas disponíveis.",
task
)
# --- Agente com Memória: contexto persiste entre sessões ---
@dataclass
class MemoryAgent:
memory_path: Path = Path(".agent_memory.json")
_mem: dict = field(default_factory=dict, init=False)
def __post_init__(self):
if self.memory_path.exists():
self._mem = json.loads(self.memory_path.read_text(encoding="utf-8"))
def remember(self, key: str, value: str) -> None:
self._mem[key] = value
self.memory_path.write_text(
json.dumps(self._mem, indent=2, ensure_ascii=False), encoding="utf-8"
)
def forget(self, key: str) -> None:
self._mem.pop(key, None)
self.memory_path.write_text(
json.dumps(self._mem, indent=2, ensure_ascii=False), encoding="utf-8"
)
def run(self, task: str) -> str:
if self._mem:
ctx = "\n".join(f"- {k}: {v}" for k, v in self._mem.items())
system = f"Você é um assistente de engenharia.\n\nCONTEXTO PERSISTIDO:\n{ctx}\n\nMantenha consistência com essas convenções."
else:
system = "Você é um assistente de engenharia. Nenhum contexto anterior disponível."
result = _agent_loop(system, task)
self.remember("ultima_tarefa", task[:200])
return result
# --- Uso comparativo ---
if __name__ == "__main__":
# Reativo: cada chamada começa sem contexto
reactive_agent("Liste os arquivos do projeto e descreva a estrutura")
# Com memória: tem contexto de sessões anteriores
agent = MemoryAgent()
agent.remember("linguagem", "Python 3.12+ com type hints obrigatórios")
agent.remember("padrao_testes", "pytest com fixtures — sem mock da camada de dados")
agent.remember("convencao_commits", "conventional commits: feat/fix/docs/refactor/test")
agent.run("Crie uma função para calcular o percentil 95 de uma lista de latências")
Contraste entre as duas arquiteturas: o agente reativo é uma função pura (sem estado entre chamadas); o MemoryAgent persiste contexto em JSON entre sessões e o injeta no system prompt de cada nova execução.
// orchestrator.go — Pipeline multi-agente com especialização por fase
package agent
import (
"context"
"fmt"
"github.com/anthropics/anthropic-sdk-go"
)
type Pipeline struct{ client anthropic.Client }
func NewPipeline() *Pipeline {
return &Pipeline{client: anthropic.NewClient()}
}
type TaskResult struct {
Context string
Spec string
Implementation string
Review string
}
// Execute — pipeline sequencial de quatro agentes especializados
func (p *Pipeline) Execute(ctx context.Context, task string) (*TaskResult, error) {
r := &TaskResult{}
var err error
// Fase 1: pesquisa de contexto — NÃO implementa, apenas lê e reporta
r.Context, err = p.run(ctx,
"Você pesquisa contexto. Leia o código existente, identifique padrões e convenções. Produza um relatório estruturado. NÃO implemente nada.",
fmt.Sprintf("Pesquise o contexto para: %s", task))
if err != nil {
return nil, fmt.Errorf("fase de contexto: %w", err)
}
// Fase 2: spec detalhado — NÃO escreve código de implementação
r.Spec, err = p.run(ctx,
"Você escreve specs. Dado objetivo e contexto, produza: comportamento esperado, edge cases, arquivos afetados, critérios de aceite. NÃO escreva código.",
fmt.Sprintf("Tarefa: %s\n\nContexto:\n%s", task, r.Context))
if err != nil {
return nil, fmt.Errorf("fase de spec: %w", err)
}
// Fase 3: implementação — segue o spec, NÃO muda escopo
r.Implementation, err = p.run(ctx,
"Você implementa specs. Dado spec e contexto, escreva código seguindo rigorosamente o spec. NÃO mude o escopo.",
fmt.Sprintf("Spec:\n%s\n\nContexto:\n%s", r.Spec, r.Context))
if err != nil {
return nil, fmt.Errorf("fase de implementação: %w", err)
}
// Fase 4: revisão — lista problemas, NÃO reescreve
r.Review, err = p.run(ctx,
"Você revisa implementações. Dado spec + código, liste discrepâncias, bugs potenciais e violações de convenção. NÃO reescreva.",
fmt.Sprintf("Spec original:\n%s\n\nImplementação:\n%s", r.Spec, r.Implementation))
if err != nil {
return nil, fmt.Errorf("fase de revisão: %w", err)
}
return r, nil
}
func (p *Pipeline) run(ctx context.Context, role, prompt string) (string, error) {
messages := []anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock(prompt)),
}
for {
msg, err := p.client.Messages.New(ctx, anthropic.MessageNewParams{
Model: anthropic.ModelClaudeOpus4_7,
MaxTokens: anthropic.Int(8192),
System: []anthropic.TextBlockParam{{Type: "text", Text: role}},
Messages: messages,
})
if err != nil {
return "", fmt.Errorf("LLM: %w", err)
}
messages = append(messages, msg.ToParam())
if msg.StopReason == "end_turn" {
for _, b := range msg.Content {
if b.Type == "text" {
return b.Text, nil
}
}
return "", fmt.Errorf("resposta sem bloco de texto")
}
}
}
Pipeline de quatro fases onde cada fase é um agente especializado com system prompt restrito ao seu papel: pesquisador coleta contexto, planejador gera spec, implementador escreve código, revisor valida contra o spec original.
Exercícios práticos
-
Trace um agente existente: Habilite o verbose mode no Claude Code (
--verbose) e execute uma tarefa não trivial — como "adicione validação de entrada nessa função". Observe o loop completo: quais ferramentas foram usadas, em que ordem, quantas vezes o loop repetiu, e o que o agente fez que você não teria previsto. Identifique: a arquitetura foi reativa ou planejadora? O agente fez planejamento explícito antes de agir? Em que ponto o contexto cresceu mais? - Implemente o MemoryAgent e use por uma semana: Implemente a versão Python (ou adapte para C# ou Go) e use-o para tarefas de engenharia reais por cinco dias. A cada dia, adicione pelo menos uma entrada de memória relevante (decisão tomada, convenção do projeto, contexto descoberto). No quinto dia: quais entradas de memória foram mais úteis? Quais eram ruído? O que você colocaria em um CLAUDE.md vs. no JSON de memória?
-
Construa uma skill: Identifique um workflow repetitivo no seu trabalho que envolva julgamento (não apenas automação mecânica) — code review, geração de changelog, análise de performance de query. Escreva uma skill em
.claude/skills/que encapsule o processo. Execute-a em cinco situações reais. Documente: onde o processo definido na skill produziu bom output? Onde o julgamento do LLM divergiu do processo esperado? O que você ajustaria no arquivo de definição?
Referências e leitura complementar
- paper ReAct: Synergizing Reasoning and Acting in Language Models — Yao et al. (2022).
- artigo Building Effective Agents — Anthropic (2024).
- paper Chain-of-Thought Prompting Elicits Reasoning in LLMs — Wei et al. (2022).
- paper Reflexion: Language Agents with Verbal Reinforcement Learning — Shinn et al. (2023).
- livro An Introduction to MultiAgent Systems — Michael Wooldridge (2009).
- docs Claude Code — Skills e Slash Commands — Anthropic (2025).
- docs Anthropic SDK — Tool Use — Anthropic (2025).
- artigo The Anatomy of Autonomy: What Makes an Agentic System Work — Latent Space (2024).
- docs LangGraph — Building Stateful Multi-Actor Applications — LangChain (2024).
- artigo Evaluating Multi-Agent Systems: The Cascade Problem — Simon Willison (2024).