5 Padrões de AI Agent - Explicação detalhada com exemplos Java
1. Visão Geral
Quando você integra um LLM com algumas linhas de código para ele “executar sozinho”, você tem um AI Agent básico — mas esse tipo de agent normalmente apenas responde o que perguntam, não sabe se corrigir, não sabe chamar ferramentas, não sabe dividir tarefas. Este artigo explica 5 padrões de design que tornam o agent mais “inteligente”: Reflection, Tool Use, Planning, Multi-Agent e ReAct. Cada padrão vem com exemplos breves de código Java (Spring AI) funcionais, acompanhados de explicações sobre quando e quando não usar. Após ler, você poderá escolher o padrão correto para seu problema, em vez de tentar colocar tudo em um prompt gigante.
Antes de entrar em detalhes, você precisará da dependência Spring AI para os exemplos abaixo:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
spring.ai.openai.api-key=${OPENAI_API_KEY}
spring.ai.openai.chat.options.model=gpt-4o-mini
Nota: O código neste artigo ilustra a estrutura dos padrões, não é um produto pronto para produção — você precisa adicionar retry, timeout, logging ao fazer deploy real.
2. Padrão Reflection: autocrítica e correção de respostas
Reflection é o padrão mais simples: o agent gera uma resposta, depois usa o próprio LLM (ou outro LLM) para avaliar/criticar essa resposta e, em seguida, corrige-a. Esse loop é semelhante a escrever um rascunho e relê-lo antes de enviar.
2.1. Fluxo de operação
- Generate: o agent gera a primeira resposta.
- Critique: o agent (ou outro agent) faz papel de “revisor”, apontando erros.
- Revise: o agent corrige a resposta com base na crítica, podendo repetir as 2–3 etapas até atingir a qualidade desejada.
@Service
public class ReflectionAgent {
private final ChatClient chatClient;
public ReflectionAgent(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
public String answerWithReflection(String question, int maxRounds) {
String draft = chatClient.prompt()
.user("Responda de forma breve a pergunta: " + question)
.call()
.content();
for (int round = 0; round < maxRounds; round++) {
String critique = chatClient.prompt()
.user("""
Você é um revisor crítico. Aponte erros e omissões na seguinte resposta.
Se estiver boa, retorne "OK".
Pergunta: %s
Resposta: %s
""".formatted(question, draft))
.call()
.content();
if (critique.strip().equalsIgnoreCase("OK")) {
break;
}
draft = chatClient.prompt()
.user("Corrija a resposta com base no comentário: " + critique
+ "\nResposta anterior: " + draft)
.call()
.content();
}
return draft;
}
}
answerWithReflection gera um rascunho inicial e depois faz um loop de até maxRounds vezes: chama o LLM fazendo o papel de revisor para avaliar; se o revisor retornar “OK”, para; caso contrário, gera uma versão corrigida. O resultado é uma resposta que tende a ser mais precisa do que chamar o LLM uma única vez.
Nota: Reflection custa 2–3 vezes mais chamadas de LLM do que o normal. Deve ser usado apenas para tarefas que exigem alta precisão (revisão de código, cálculos, redação de contratos), não para chat comum, pois é lento e caro.
3. Padrão Tool Use: permitindo que o agent chame ferramentas externas
O LLM não sabe a hora atual, não consegue acessar databases, não calcula números grandes com precisão. O padrão Tool Use permite que o agent “chame funções” quando necessário — o LLM decide qual ferramenta chamar, o código executa essa ferramenta e retorna o resultado para o LLM continuar o raciocínio.
@Component
public class OrderTools {
private final OrderRepository orderRepository;
public OrderTools(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
@Tool(description = "Consultar o status do pedido pelo código do pedido")
public String getOrderStatus(String orderId) {
return orderRepository.findById(orderId)
.map(Order::getStatus)
.orElse("Pedido não encontrado " + orderId);
}
@Tool(description = "Calcular o total do pedido incluindo imposto de 10%")
public double calculateTotal(double subtotal) {
return subtotal * 1.1;
}
}
@Service
public class CustomerSupportAgent {
private final ChatClient chatClient;
public CustomerSupportAgent(ChatClient.Builder builder, OrderTools orderTools) {
this.chatClient = builder
.defaultTools(orderTools)
.build();
}
public String handle(String userMessage) {
return chatClient.prompt()
.user(userMessage)
.call()
.content();
}
}
@Tool marca o método como uma função que o LLM pode chamar; description ajuda o modelo a entender quando deve usá-la. Quando defaultTools(orderTools) é registrado, Spring AI automaticamente: envia o schema das ferramentas para o LLM, recebe a solicitação de chamada da ferramenta (em formato JSON), executa o método Java correspondente e envia o resultado de volta ao LLM para completar a resposta.
Por exemplo, se o usuário pergunta “Onde está meu pedido ORD-001?”, o agent decide chamar getOrderStatus("ORD-001") automaticamente, sem precisar que você escreva a lógica de parse de intent manualmente.
User: Onde está meu pedido ORD-001?
Agent: O pedido ORD-001 está atualmente no status "Entrega em andamento".
Test simples com JUnit 5 + Mockito para validar que a ferramenta funciona corretamente, sem precisar chamar o LLM real:
class OrderToolsTest {
private final OrderRepository repository = mock(OrderRepository.class);
private final OrderTools tools = new OrderTools(repository);
@Test
void shouldReturnStatusWhenOrderExists() {
when(repository.findById("ORD-001"))
.thenReturn(Optional.of(new Order("ORD-001", "Entrega em andamento")));
String result = tools.getOrderStatus("ORD-001");
assertThat(result).isEqualTo("Entrega em andamento");
}
}
Nota: Sempre valide a entrada antes da ferramenta acessar o database ou chamar APIs reais — o LLM pode passar parâmetros com tipo errado (por exemplo, orderId vazio) se o prompt não for claro.
4. Padrão Planning: dividir tarefas grandes em passos menores
Quando há requisições complexas (“escrever um relatório comparando 3 fornecedores, com tabela de preços e recomendações”), pedir ao LLM fazer tudo de uma vez tende a pular passos. O padrão Planning separa em dois papéis: Planner cria a lista de passos e Executor executa cada passo sequencialmente.
public record PlanStep(int order, String description) {}
@Service
public class PlannerAgent {
private final ChatClient chatClient;
public PlannerAgent(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
public List<PlanStep> createPlan(String goal) {
String json = chatClient.prompt()
.user("""
Divida o seguinte objetivo em até 5 passos de execução,
retornando um JSON array no formato [{"order":1,"description":"..."}]:
%s
""".formatted(goal))
.call()
.content();
return parseSteps(json);
}
}
@Service
public class ExecutorAgent {
private final ChatClient chatClient;
public ExecutorAgent(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
public String runPlan(String goal, List<PlanStep> steps) {
StringBuilder context = new StringBuilder();
for (PlanStep step : steps) {
String result = chatClient.prompt()
.user("Objetivo: " + goal
+ "\nContexto do que já foi feito: " + context
+ "\nExecute o passo: " + step.description())
.call()
.content();
context.append("Passo ").append(step.order())
.append(": ").append(result).append("\n");
}
return context.toString();
}
}
PlannerAgent é responsável apenas por gerar a lista de passos em JSON; ExecutorAgent executa cada passo e acumula context para que o próximo passo saiba o que o anterior fez. Separar planner/executor facilita o debug: se o resultado estiver errado, primeiro valide o plano (é razoável?) e depois analise cada passo de execução.
Plano para "escrever um relatório comparando 3 fornecedores":
1. Coletar informações de preço e características de cada fornecedor
2. Criar uma tabela comparativa
3. Analisar pontos fortes e fracos
4. Fazer recomendações
5. Escrever o resumo executivo
Nota: O padrão Planning funciona bem quando a tarefa tem múltiplos passos interdependentes. Para perguntas de um passo (por exemplo, “resuma este parágrafo”), adicionar um planner só torna as coisas mais lentas e desperdiça tokens desnecessariamente.
5. Padrão Multi-Agent: vários agents especializados trabalhando juntos
Em vez de um único agent “que sabe tudo”, o padrão Multi-Agent divide o trabalho entre agents especializados: agent de pesquisa, agent de escrita, agent de revisão… e um Orchestrator que coordena o fluxo entre eles. Como um modelo de pequena equipe, onde cada um é bom em uma coisa.
@Service
public class ResearchAgent {
private final ChatClient chatClient;
public ResearchAgent(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
public String research(String topic) {
return chatClient.prompt()
.user("Liste os 5 pontos mais importantes sobre: " + topic)
.call()
.content();
}
}
@Service
public class WriterAgent {
private final ChatClient chatClient;
public WriterAgent(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
public String write(String topic, String researchNotes) {
return chatClient.prompt()
.user("Escreva um artigo de 200 palavras sobre '"
+ topic + "' com base nos dados: " + researchNotes)
.call()
.content();
}
}
@Service
public class OrchestratorAgent {
private final ResearchAgent researchAgent;
private final WriterAgent writerAgent;
public OrchestratorAgent(ResearchAgent researchAgent, WriterAgent writerAgent) {
this.researchAgent = researchAgent;
this.writerAgent = writerAgent;
}
public String produceArticle(String topic) {
String notes = researchAgent.research(topic);
return writerAgent.write(topic, notes);
}
}
OrchestratorAgent não chama o LLM diretamente, apenas orquestra: chama ResearchAgent para obter os dados brutos e passa para WriterAgent escrever o artigo final. Cada agent tem seu próprio prompt, otimizado para uma tarefa específica, então a qualidade de cada parte tende a ser melhor do que colocar tudo em um prompt longo.
Test com mock para validar que o fluxo de coordenação está correto, sem depender do LLM:
class OrchestratorAgentTest {
@Test
void shouldPassResearchNotesToWriter() {
ResearchAgent research = mock(ResearchAgent.class);
WriterAgent writer = mock(WriterAgent.class);
when(research.research("Spring AI")).thenReturn("note-A, note-B");
when(writer.write("Spring AI", "note-A, note-B")).thenReturn("Artigo completo");
OrchestratorAgent orchestrator = new OrchestratorAgent(research, writer);
String article = orchestrator.produceArticle("Spring AI");
assertThat(article).isEqualTo("Artigo completo");
verify(writer).write("Spring AI", "note-A, note-B");
}
}
Nota: Multi-Agent aumenta a latência (múltiplas chamadas de LLM em sequência) e a complexidade do sistema. Divida os agents apenas quando cada um tiver prompt/contexto claramente diferente — dividir em excesso (cada agent com 1 frase) só causa overhead desnecessário.
6. Padrão ReAct: raciocínio intercalado com ação
ReAct (Reasoning + Acting) é um padrão que combina Tool Use com um loop de raciocínio: o agent pensa (Thought) → atua (Action, por exemplo, chamando uma ferramenta) → observa o resultado (Observation) → repete até ter informações suficientes para responder. Diferente de Tool Use puro (chamar 1 ferramenta e depois responder), ReAct permite que o agent chame múltiplas ferramentas em sequência flexível, decidindo o próximo passo com base na observação anterior.
@Service
public class ReActAgent {
private final ChatClient chatClient;
private final List<Object> tools;
public ReActAgent(ChatClient.Builder builder, WeatherTool weatherTool, SearchTool searchTool) {
this.tools = List.of(weatherTool, searchTool);
this.chatClient = builder.defaultTools(tools.toArray()).build();
}
public String solve(String question) {
return chatClient.prompt()
.system("""
Quando precisar de dados reais, chame a ferramenta apropriada antes de responder.
Raciocine passo a passo: Thought -> Action -> Observation -> Thought...
Forneça a resposta final apenas quando tiver informações suficientes.
""")
.user(question)
.call()
.content();
}
}
Com a pergunta “Devo levar uma jaqueta de chuva para Đà Lạt neste fim de semana?”, o agent pode raciocinar sozinho: chamar weatherTool.forecast("Đà Lạt") para obter a previsão, observar o resultado “70% de chance de chuva” e depois responder — toda a sequência Thought/Action/Observation acontece dentro do framework sem precisar escrever o loop manualmente, porque Spring AI cuida do loop de chamadas de ferramentas até que o modelo decida parar.
Thought: Preciso saber o tempo em Đà Lạt neste fim de semana.
Action: forecast("Đà Lạt")
Observation: 70% de chuva, 18-22°C
Thought: Devo levar uma jaqueta de chuva.
Answer: Você deve levar uma jaqueta de chuva; a previsão para Đà Lạt neste fim de semana é 70% de chance de chuva.
Nota: ReAct pode facilmente entrar em “loop infinito” se o agent continuar chamando ferramentas sem convergir para uma resposta. Sempre defina um limite máximo de chamadas de ferramenta (
maxToolCallsou timeout) para evitar gastar dinheiro em API fora de controle.
7. Comparação rápida dos 5 padrões
| Pattern | Resolve qual problema | Custo (número de chamadas LLM) | Quando usar |
|---|---|---|---|
| Reflection | Qualidade de resposta baixa | Alto (2–3x) | Tarefas que exigem precisão, podem se auto-validar |
| Tool Use | LLM não consegue acessar dados/sistemas reais | Baixo–médio | Precisa de consulta, cálculos, chamadas de API |
| Planning | Tarefas com múltiplos passos, fáceis de pular | Médio | Tarefas complexas, estruturadas claramente |
| Multi-Agent | Um agent não é especializado o suficiente para tudo | Alto | Pipeline com papéis claros (pesquisa/escrita/revisão) |
| ReAct | Precisa de ferramentas mas número de passos é desconhecido | Variável | Tarefas que precisam explorar informações de forma flexível |
Na prática, esses padrões não são mutuamente exclusivos — um agent de suporte ao cliente pode usar Tool Use para consultar pedidos, Planning para lidar com requisições complexas e Reflection para rever respostas sensíveis antes de enviar.
8. Conclusão
Os 5 padrões acima — Reflection, Tool Use, Planning, Multi-Agent, ReAct — são blocos básicos para construir AI Agents capazes de se autocorrigir, interagir com sistemas reais, lidar com tarefas complexas e explorar informações de forma flexível. Você não precisa usar todos os 5 em todos os problemas; comece com Tool Use (praticamente todo agent precisa), depois adicione Planning ou ReAct quando a tarefa fica mais complexa, e use Reflection/Multi-Agent apenas quando o custo adicional vale a qualidade. Escolher o padrão certo para o problema é mais importante do que tentar colocar tudo em um único agent.