5 Padrões de AI Agent - Explicação detalhada com exemplos Java

Este artigo tem um vídeo curto:YouTubeTikTok

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

  1. Generate: o agent gera a primeira resposta.
  2. Critique: o agent (ou outro agent) faz papel de “revisor”, apontando erros.
  3. 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 (maxToolCalls ou 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.

ai-agentspring-aijavallmdesign-pattern