DFCODE

Tecnologia e Inovação

DFCODE — Escola de Tecnologia

Spring AI em Aplicações Java

Integração com LLMs no Spring Boot — do ChatClient ao RAG,
tool calling e MCP, com exemplos e exercícios
8 capítulos+ introdução
Trilha práticaOpenAI, modelos, produção
Spring Boot 3.xJava 17+
Ebook DFCODE2026
📚 Como usar este ebook

Use o menu lateral fixo para navegar entre a capa, o índice completo e cada capítulo — o menu permanece visível ao rolar a página. Os números em verde no menu indicam a ordem didática (00 a 08). Configure chaves de API com variáveis de ambiente e consulte a documentação oficial do Spring AI para versões atuais.

Parte 0 — Contexto: inteligência artificial aplicada e engenharia de prompt

Antes de mergulhar no código, vale alinhar o que é usar IA em um backend e como formular solicitações ao modelo para obter respostas úteis e previsíveis.


0.1 O que muda quando você coloca IA no Spring?

Em muitas aplicações Java, a regra de negócio está no seu código e nos dados que você controla. Com modelos de linguagem (LLMs), parte da “resposta” vem de um serviço externo que você chama por API: você envia texto, o modelo devolve texto (ou outro formato).

Fluxo típico:

  1. Entrada — usuário ou sistema envia uma pergunta ou comando.
  2. Seu backend monta a mensagem (prompt), opcionalmente com instruções de sistema.
  3. Provedor de modelo processa e devolve a saída.
  4. Seu backend valida, formata e devolve a resposta ao chamador (por exemplo, app ou API).

O Spring AI existe para não acoplar você a detalhes de HTTP e JSON de cada fornecedor e para permitir trocar modelo ou provedor com menos atrito. Visão geral: Spring AI Reference — Introduction.


0.2 IA generativa em uma frase

IA generativa aqui significa: gerar texto novo (e, em outros capítulos, imagem ou áudio) a partir de uma entrada, em vez de só classificar ou pontuar dados com regras fixas. O modelo foi treinado em grandes volumes de texto; na inferência (quando você chama a API), ele prevê a próxima sequência de tokens — por isso respostas podem variar e, às vezes, confundir ou inventar se o contexto for fraco.

Exemplo mental

  • Regra fixa: “Se o chamado está aberto há mais de 48h, escalar.” → sempre determinístico.
  • Generativo: “Explique em duas frases o que significa escalar o chamado 742.” → pode variar na redação; você controla com prompt e parâmetros.

0.3 O que é o “modelo” e o que é “prompt”?

Termo Em linguagem simples
Modelo O “motor” que recebe a entrada e gera a saída (ex.: GPT-4.x, o modelo que você configurar no Spring AI).
Prompt O texto que você envia para orientar a tarefa: pergunta, instrução, formato desejado.
Mensagem de sistema Instruções estáveis: papel do assistente, tom, restrições (“não invente dados”).
Mensagem do usuário A solicitação concreta da vez.

Mini-exemplo

Sistema: Você é um assistente de suporte técnico. Responda em português, em no máximo 3 frases.
Usuário: Como redefino a senha?

O Capítulo 1 implementa isso com ChatClient; o Capítulo 2 aprofunda parâmetros e testes no Playground.


0.4 Engenharia de prompt (antes do código)

Engenharia de prompt é o desenho cuidadoso do que você envia ao modelo para obter:

  • resposta no formato que seu sistema consome (JSON, lista, uma palavra só);
  • resposta alinhada ao domínio (suporte, escola, financeiro);
  • menor risco de invenção ou fuga de assunto.

Princípios práticos:

  1. Seja específico — “Devolva apenas uma categoria: A, B ou C” é melhor que “classifique”.
  2. Diga o que não pode — “Se não houver dados no contexto, diga que não sabe.”
  3. Mostre o formato — um exemplo de saída desejada reduz ambiguidade.
  4. Combine com parâmetros — temperatura baixa para tarefas mais determinísticas (ver Cap. 2).

Antes / depois (didático)

Menos adequado Mais adequado para backend
“Fale sobre o assunto.” “Liste 3 benefícios em bullet points, máximo 50 palavras por item.”
“Qual a categoria?” “Responda com uma única etiqueta: Acesso, Bug, Faturamento ou Outros.”

O Capítulo 2 liga isso à API da OpenAI e ao Java com Spring AI.


0.5 Limites e responsabilidade

  • Modelos não têm acesso automático ao seu banco de dados: para isso existem tools (Cap. 6), MCP (Cap. 7) e RAG (Cap. 8).
  • Chaves de API são segredos: use variáveis de ambiente (Cap. 1).
  • Custos e tokens dependem do modelo e do tamanho das mensagens (Cap. 3).

Quando estiver confortável com esses conceitos, siga para o Capítulo 1 — Primeiros passos com Spring AI.


Capítulo 1 — Primeiros passos com Spring AI: entendendo a integração com IA e criando a primeira aplicação

Objetivo do capítulo

Neste capítulo, você vai aprender:

  • o que é o Spring AI e por que ele facilita o desenvolvimento com IA;
  • quais conceitos básicos você precisa entender antes de escrever código;
  • como criar um projeto Spring Boot preparado para usar IA;
  • como fazer a primeira chamada a um modelo com ChatClient;
  • o que é uma API Key e por que ela precisa ser protegida;
  • como configurar a chave com segurança usando variável de ambiente;
  • como testar a aplicação localmente.

Ao final, a ideia é simples: você deve conseguir ler o capítulo, entender o fluxo e repetir o exemplo sozinho.


1.1 Antes do código: o que significa usar IA dentro de uma aplicação?

Quando usamos uma biblioteca comum no Java, grande parte do processamento acontece dentro do próprio projeto. Mas, no caso de modelos de IA, normalmente sua aplicação conversa com um serviço externo. Em vez de “rodar tudo localmente”, o sistema envia uma mensagem para um modelo, aguarda o processamento e recebe uma resposta.

Na prática, o fluxo é este:

  1. o usuário faz uma ação;
  2. o backend monta uma solicitação;
  3. essa solicitação é enviada ao modelo;
  4. o modelo processa a entrada;
  5. a resposta volta para a aplicação;
  6. o sistema entrega o resultado para o usuário.
sequenceDiagram autonumber actor U as Usuário participant B as Backend(Spring AI) participant M as Modelo de IA(OpenAI / outro) U->>B: Faz uma ação (ex: chama endpoint) B->>B: Monta o prompt e as opções B->>M: Envia solicitação (prompt + configurações) M->>M: Processa a entrada M-->>B: Retorna a resposta gerada B-->>U: Entrega o resultado ao usuário

Esse fluxo parece simples, mas envolve alguns pontos importantes:

  • como montar a mensagem correta;
  • como autenticar a aplicação;
  • como organizar o código;
  • como controlar o comportamento da resposta;
  • como evitar acoplamento excessivo a um provedor específico.

É exatamente aqui que o Spring AI entra.


1.2 O que é o Spring AI?

O Spring AI é um framework voltado para engenharia de aplicações com inteligência artificial. O objetivo dele é aplicar no universo de IA princípios que já são comuns no ecossistema Spring, como modularidade, portabilidade e uso de componentes simples para montar a aplicação.

Explicando de forma prática

Sem uma abstração como o Spring AI, você poderia precisar:

  • montar requisições HTTP manualmente;
  • lidar diretamente com autenticação;
  • configurar cabeçalhos e corpo JSON;
  • tratar respostas brutas;
  • acoplar sua aplicação a detalhes específicos de um provedor.

Com Spring AI, parte dessa complexidade já vem organizada em APIs mais amigáveis.

O que isso traz de vantagem?

O Spring AI oferece abstrações para trabalhar com recursos de IA e permite trocar componentes com mudanças menores no código, além de suportar APIs portáveis entre provedores para chat, embeddings e text-to-image, com opções síncronas e streaming.

Em termos simples, ele ajuda você a escrever um código mais limpo, mais fácil de entender e mais alinhado com o estilo de desenvolvimento do Spring.


1.3 O que o Spring AI cobre além de chat?

Embora muita gente comece usando apenas geração de texto, o ecossistema do Spring AI é mais amplo.

Ele cobre recursos como:

  • chat completion;
  • embeddings;
  • text-to-image;
  • transcription;
  • text-to-speech;
  • moderation.

O que cada um desses recursos significa?

Chat completion É o recurso mais comum. Você envia uma mensagem (ou uma conversa) e o modelo gera uma resposta textual. É o que usamos para criar assistentes, responder perguntas, gerar conteúdo e muito mais.

Entrada: "Explique o que é REST em duas frases."
Saída:   "REST é um estilo arquitetural para APIs web. Ele usa HTTP e opera sobre recursos identificados por URLs."

Embeddings Em vez de gerar texto, o modelo transforma um texto em um vetor numérico (uma lista de números). Esse vetor representa o significado do texto e é usado para comparar similaridade entre frases, fazer busca semântica e alimentar bancos vetoriais (como no RAG, visto no Capítulo 8).

Entrada: "Como faço matrícula?"
Saída:   [0.021, -0.134, 0.876, ...]  ← representação vetorial do significado

Text-to-image O modelo recebe uma descrição em texto e gera uma imagem. Útil para criação de banners, ilustrações e protótipos visuais. O Capítulo 5 mostra como usar isso com ImageModel no Spring AI.

Entrada: "Um robô lendo um livro de Java, estilo cartoon"
Saída:   URL de uma imagem gerada

Transcription O modelo recebe um arquivo de áudio e converte o conteúdo falado em texto escrito. Útil para transcrever reuniões, aulas, atendimentos e gravações.

Entrada: arquivo .mp3 com fala
Saída:   "Bom dia, gostaria de abrir uma conta..."

Text-to-speech O oposto da transcrição: o modelo recebe texto e gera áudio com voz sintetizada. Útil para leitores de acessibilidade, narrações automáticas e respostas por voz.

Entrada: "Sua solicitação foi registrada com sucesso."
Saída:   arquivo de áudio com a frase narrada

Moderation O modelo analisa um texto e verifica se ele contém conteúdo inadequado (violência, discurso de ódio, conteúdo explícito etc.). Útil para validar entradas de usuários antes de processar ou armazenar.

Entrada: "Texto enviado pelo usuário"
Saída:   { "flagged": false, "categories": { "hate": false, "violence": false } }

Visão geral dos recursos do Spring AI

flowchart LR SA["Spring AI"] SA --> CC["Chat CompletionGera texto"] SA --> EM["EmbeddingsGera vetores numericos"] SA --> TI["Text-to-ImageGera imagens"] SA --> TR["TranscriptionAudio para Texto"] SA --> TS["Text-to-SpeechTexto para Audio"] SA --> MO["ModerationFiltra conteudo"]

Para este início de curso, vamos focar em chat, porque é o caminho mais fácil para entender a integração. Mas é importante você saber, desde já, que o framework não se limita a perguntas e respostas.


1.4 Conceitos que você precisa entender antes de programar

Antes de partir para o projeto, existem alguns conceitos fundamentais.

Modelo

Um modelo de IA é o componente que recebe uma entrada e gera uma saída.

Para o nosso contexto, pense no modelo como o “motor” que recebe a mensagem e produz a resposta.

Prompt

Prompt é a instrução enviada ao modelo. É o texto que orienta a resposta.

Exemplo:

Sugira 5 títulos curtos para posts de blog sobre Java e Spring Boot, em português.

O modelo vai interpretar essa instrução e produzir uma saída com base nela.

Mensagem de sistema

A mensagem de sistema serve para orientar o comportamento da conversa.

Exemplo:

Você é editor técnico: títulos objetivos, sem clickbait, para público desenvolvedor.

Isso ajuda o modelo a entender “como” ele deve se comportar.

Mensagem do usuário

É a solicitação direta enviada pela pessoa usuária.

Exemplo:

Inclua a palavra “tutorial” em pelo menos dois títulos da lista.

Resposta

É o conteúdo gerado pelo modelo com base nas instruções recebidas.

Opções do modelo

São configurações que afetam a chamada, como modelo usado, temperatura e outras preferências de geração.


1.5 Por que o ChatClient é importante?

O ChatClient é uma das peças centrais do Spring AI.

Em linguagem simples

Você pode pensar no ChatClient como o “canal de conversa” entre sua aplicação e o modelo.

Em vez de montar uma chamada complexa manualmente, você usa uma API fluente que organiza isso de forma mais natural.


1.6 Criando o projeto

Agora que a base conceitual está mais clara, podemos ir para a implementação.

Estrutura sugerida

  • Build tool: Maven
  • Language: Java
  • Packaging: Jar
  • Java: 21
  • Dependências principais:
    • Spring Web
    • Spring AI com OpenAI
    • DevTools

Exemplo de dependências

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
</dependencies>

Esse conjunto já entrega uma base suficiente para criar endpoints e chamar um modelo.


1.7 Entendendo a classe principal da aplicação

Todo projeto Spring Boot possui uma classe principal com @SpringBootApplication. Essa classe é o ponto de entrada do sistema.

Exemplo

package br.com.seuprojeto.app;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class AplicacaoApplication {

    public static void main(String[] args) {
        SpringApplication.run(AplicacaoApplication.class, args);
    }
}

O que acontece quando ela é executada?

Quando esse main() roda:

  1. o Spring Boot inicia a aplicação;
  2. carrega as configurações;
  3. identifica os componentes anotados;
  4. prepara os beans;
  5. tenta montar a integração com IA;
  6. sobe o servidor da aplicação.
flowchart TD A(["Executa main"]) --> B["Spring Boot inicia"] B --> C["Carrega configuracoesapplication.properties e variaveis de ambiente"] C --> D["Identifica componentes anotadosRestController, Service, Component"] D --> E["Prepara os beansChatClient, ImageModel"] E --> F{"API Key configurada?"} F -->|"Sim"| G["Monta integracao com IASpring AI conecta ao provedor"] F -->|"Nao"| ERR["Erro na inicializacaoBeanCreationException"] G --> H(["Servidor HTTP no ar"])

Dica: se a aplicação não subir e aparecer BeanCreationException ou IllegalStateException, verifique primeiro se a variável de ambiente com a API Key está definida. O Spring AI tenta configurar o cliente do provedor durante o boot e falha se a chave estiver ausente.


1.8 Criando a primeira funcionalidade com IA

Um primeiro exemplo simples é pedir sugestões de títulos para conteúdo técnico via um endpoint REST.

O objetivo desse primeiro exemplo

Queremos que:

  • o usuário acesse uma rota;
  • o backend envie um prompt ao modelo;
  • o modelo gere uma resposta textual;
  • essa resposta volte ao navegador.
flowchart LR U(["Usuário / Navegador"]) -->|"GET /gerador"| C["GeradorController"] C -->|"monta o prompt"| P["prompt: títulos para blog Java/Spring"] P -->|"ChatClient envia"| M["Modelo de IA"] M -->|"resposta textual"| C C -->|"retorna texto"| U

1.9 Criando o controller

package br.com.seuprojeto.app.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/gerador")
public class GeradorController {
}

O que esse código significa?

@RestController
Diz ao Spring que a classe responderá requisições HTTP e retornará dados diretamente no corpo da resposta.

@RequestMapping("/gerador")
Define a rota base que será usada.


1.10 Inserindo um método simples

Antes de falar com o modelo, podemos testar a rota com um retorno simples.

@GetMapping
public String gerarTitulos() {
    return "Funcionando";
}

Isso ajuda Você a validar o primeiro passo: a aplicação está subindo e o endpoint está respondendo.


1.11 Fazendo a primeira chamada com ChatClient

Exemplo completo

package br.com.seuprojeto.app.controller;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/gerador")
public class GeradorController {

    private final ChatClient chatClient;

    public GeradorController(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }

    @GetMapping
    public String gerarTitulos() {
        String pergunta = "Sugira 5 títulos curtos para posts de blog sobre Java e Spring Boot, em português.";

        return chatClient.prompt()
                .user(pergunta)
                .call()
                .content();
    }
}

1.12 Explicando o código linha por linha

private final ChatClient chatClient;

Aqui declaramos o cliente que será usado para conversar com o modelo.

public GeradorController(ChatClient.Builder chatClientBuilder)

O Spring injeta um Builder já configurado.

this.chatClient = chatClientBuilder.build();

O builder gera a instância final do cliente.

String pergunta = "Sugira 5 títulos curtos para posts de blog sobre Java e Spring Boot, em português.";

Esse é o prompt do usuário.

chatClient.prompt()

Inicia a montagem da chamada.

.user(pergunta)

Define a mensagem enviada pelo usuário.

.call()

Executa a chamada ao modelo.

.content()

Extrai o texto da resposta.

Visão do encadeamento (fluent API)

flowchart TD A["chatClient.prompt()"] --> B[".user(pergunta)"] B --> C[".call()"] C --> D[".content()"] A --- N1["inicia a montagem da chamada"] B --- N2["define a mensagem do usuario"] C --- N3["dispara a requisicao ao modelo"] D --- N4["extrai o texto da resposta"]

Cada método retorna o próximo objeto da cadeia. Você lê a chamada de cima para baixo como se fosse uma frase: "prepare um prompt, com esta mensagem de usuário, chame o modelo e me dê o conteúdo."


1.13 O que é uma API Key?

Para que sua aplicação consiga usar um serviço externo de IA, ela precisa se autenticar.

Em linguagem simples

A API Key funciona como uma credencial de acesso da aplicação.

Ela serve para:

  • identificar quem está fazendo a chamada;
  • autorizar o uso da API;
  • permitir controle de consumo;
  • associar o uso à conta correta.

1.14 Por que essa chave precisa ser protegida?

A chave de API é sensível. Se ela for exposta, outra pessoa pode usar a API como se fosse a sua aplicação.

Isso pode causar:

  • consumo indevido;
  • custos inesperados;
  • necessidade de revogar a chave;
  • impacto operacional no sistema.

Por isso, não é boa prática gravar a chave diretamente no código-fonte.


1.15 Configurando a chave no Spring

Exemplo recomendado

spring.application.name=meu-projeto
spring.ai.openai.api-key=${OPENAI_API_KEY}

Essa abordagem evita expor a chave diretamente no projeto.


1.16 O que é uma variável de ambiente?

Uma variável de ambiente é um valor armazenado fora do código, no ambiente onde a aplicação está rodando.

Ela é muito usada para guardar:

  • senhas;
  • tokens;
  • chaves de API;
  • URLs;
  • configurações que mudam de um ambiente para outro.

Vantagens

Mais segurança
A credencial não fica escrita dentro do código.

Mais flexibilidade
Você pode usar uma chave em desenvolvimento e outra em produção sem alterar a aplicação.

Mais organização
O mesmo código pode rodar em ambientes diferentes mudando apenas a configuração externa.


1.17 Exemplo do que não fazer

Errado

spring.ai.openai.api-key=sk-valor-real-da-chave

Certo

spring.ai.openai.api-key=${OPENAI_API_KEY}

1.18 Onde configurar essa variável?

Em desenvolvimento local, você pode configurar a variável na IDE ou no sistema operacional.

Em projetos reais, a variável também pode ser definida em:

  • terminal;
  • servidor;
  • container Docker;
  • Kubernetes;
  • pipeline de CI/CD.

1.19 O que acontece se a chave não for configurada?

Se a chave não estiver disponível, o Spring AI não consegue completar a configuração do cliente da OpenAI.

Didaticamente, isso é importante porque mostra:

  • a dependência está correta;
  • o projeto reconheceu a integração;
  • o problema não está no controller;
  • o que falta é autenticação.

1.20 Testando a aplicação

Depois de configurar a variável de ambiente e subir a aplicação, você pode testar a rota no navegador.

Exemplo

GET http://localhost:8080/gerador

Se tudo estiver correto, a aplicação deve retornar uma resposta gerada pela IA.


1.21 Exemplo consolidado do primeiro projeto

application.properties

spring.application.name=meu-projeto
spring.ai.openai.api-key=${OPENAI_API_KEY}
server.port=8080

Classe principal

package br.com.seuprojeto.app;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class AplicacaoApplication {

    public static void main(String[] args) {
        SpringApplication.run(AplicacaoApplication.class, args);
    }
}

Controller

package br.com.seuprojeto.app.controller;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/gerador")
public class GeradorController {

    private final ChatClient chatClient;

    public GeradorController(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }

    @GetMapping
    public String gerarTitulos() {
        String prompt = """
                Sugira 5 títulos curtos para posts de blog sobre Java e Spring Boot, em português.
                Responda em português.
                Retorne apenas a lista.
                """;

        return chatClient.prompt()
                .user(prompt)
                .call()
                .content();
    }
}

1.22 O que você precisa entender desse exemplo?

Mais importante do que decorar o código é entender o fluxo:

  1. o usuário chama /gerador;
  2. o controller recebe a requisição;
  3. o método monta o prompt;
  4. o ChatClient envia a solicitação ao modelo;
  5. o modelo responde;
  6. o backend devolve o texto.
sequenceDiagram autonumber actor U as Usuário(Navegador) participant C as GeradorController participant CC as ChatClient participant M as Modelo de IA U->>C: GET /gerador C->>C: Monta o prompt C->>CC: chatClient.prompt().user(...).call() CC->>M: Envia requisição HTTP ao provedor M-->>CC: Retorna resposta JSON CC-->>C: .content() extrai o texto C-->>U: Retorna o texto ao navegador

Quando esse fluxo fica claro, você deixa de apenas copiar e passa a realmente aplicar.


1.23 Erros comuns no começo

Erro 1: achar que basta adicionar a dependência

Não basta. A integração também precisa de configuração da chave.

Erro 2: colocar a API Key direto no código

Funciona tecnicamente, mas é inseguro.

Erro 3: não testar a rota antes da integração

Primeiro valide se o endpoint responde. Depois conecte a IA.

Erro 4: copiar código sem entender o papel de cada parte

O importante é saber o que cada linha faz.

Erro 5: achar que prompt é “qualquer string”

O prompt é a instrução que orienta o modelo. A forma como ele é escrito influencia o resultado.


1.24 Boas práticas desde o primeiro capítulo

  • mantenha a chave fora do código-fonte;
  • use variável de ambiente;
  • teste a rota localmente;
  • escreva prompts claros;
  • comece com exemplos pequenos;
  • entenda o fluxo antes de avançar para casos mais complexos.

1.25 Resumo do capítulo

Neste capítulo, você aprendeu a base necessária para começar com Spring AI.

Vimos que:

  • o Spring AI existe para simplificar a integração entre aplicações e modelos de IA;
  • o framework cobre mais do que chat, incluindo embeddings, imagem, áudio e outros recursos;
  • ChatClient é a principal porta de entrada para conversar com modelos de chat;
  • prompts são compostos por mensagens, especialmente de sistema e de usuário;
  • a API Key é necessária para autenticar a aplicação com a OpenAI;
  • a forma recomendada de configurar a chave é via variável de ambiente;
  • o primeiro exemplo com controller e ChatClient já permite entender o fluxo completo da integração.

1.26 Exercícios propostos

1. Conceitual
Explique com suas palavras qual problema o Spring AI ajuda a resolver.

2. Conceitual
Qual a diferença entre mensagem de sistema e mensagem do usuário?

3. Prático
Crie um endpoint que peça ao modelo 3 nomes para uma escola de tecnologia.

4. Prático
Altere o prompt para que a resposta venha em português e em lista numerada.

5. Segurança
Explique por que a API Key não deve ser salva diretamente no arquivo de configuração com valor fixo.


Desafio extra

Crie um segundo endpoint GET /gerador/sobre que envie ao modelo um system fixo (“Você explica conceitos de Spring em linguagem simples”) e uma pergunta vinda de query string (?q=...). Mostre um exemplo de chamada no navegador e um exemplo de resposta.


1.27 Fechamento

Agora você já tem a base para começar a trabalhar com IA em uma aplicação Spring de maneira organizada.

Esse primeiro passo é essencial porque constrói três pilares:

  • entendimento conceitual;
  • implementação prática;
  • segurança mínima da integração.

Com essa base pronta, o próximo capítulo pode avançar para um tema muito importante: como controlar melhor o comportamento da IA, explorando parâmetros, Playground e engenharia de prompt.


Capítulo 2 — Explorando a API da OpenAI: parâmetros, Playground, triagem de chamados e engenharia de prompt

Objetivo do capítulo

Neste capítulo, você vai aprender:

  • por que nem toda resposta da IA vem no formato ideal para uma aplicação;
  • como usar o Playground para testar ideias antes de programar;
  • o que são parâmetros como temperatura e como eles afetam a resposta;
  • como transportar uma configuração testada para o código Java com Spring AI;
  • por que respostas não padronizadas podem atrapalhar a aplicação;
  • como usar mensagens de sistema e prompt engineering para guiar melhor o modelo;
  • como construir prompts mais claros, restritos e úteis para o backend.

Ao final, você deve ser capaz de olhar para uma funcionalidade com IA e pensar assim:

“Não basta funcionar. A resposta precisa vir do jeito certo para o sistema conseguir usar.”


2.1 Introdução

No capítulo anterior, fizemos a primeira integração entre a aplicação e o modelo de IA. O resultado foi importante: conseguimos enviar um prompt, chamar o modelo e receber uma resposta.

Mas, em aplicações reais, isso é só o começo.

Quando começamos a usar IA dentro de um sistema, logo aparece um novo desafio:

a resposta pode estar correta, mas ainda assim vir em um formato ruim para a aplicação.

Por exemplo, imagine que seu sistema precise receber apenas uma categoria, como:

Esportes

Mas o modelo responde assim:

Categoria principal: Esportes. Subcategorias possíveis: futebol, vôlei, corrida e acessórios esportivos.

Para um ser humano, isso pode parecer uma boa resposta.
Para um sistema, pode ser um problema.

Isso acontece porque o modelo não sabe, sozinho, o nível de rigidez que sua aplicação exige. Por isso, além de integrar com a IA, você precisa aprender a:

  • ajustar parâmetros;
  • testar comportamento antes de codificar;
  • escrever prompts mais específicos;
  • restringir formato de saída.

É exatamente isso que veremos neste capítulo.


2.2 Por que explorar a API antes de alterar o código?

Quando o desenvolvedor já tem um projeto funcionando, é comum pensar:

“Vou ajustar tudo direto no código.”

Isso funciona, mas nem sempre é a melhor estratégia.

Quando estamos lidando com IA, existem muitas variáveis ao mesmo tempo:

  • o texto do prompt;
  • o papel definido no system;
  • o nível de criatividade;
  • a quantidade de texto gerada;
  • a consistência da saída;
  • o formato da resposta.

Se cada teste exigir:

  1. alterar código;
  2. subir a aplicação;
  3. chamar o endpoint;
  4. comparar a resposta;
  5. repetir tudo de novo;

o processo fica mais lento do que precisa.

Por isso, antes de fixar a implementação, vale explorar o comportamento da API em um ambiente de simulação.

Ideia principal

O Playground ajuda você a responder perguntas como:

  • essa temperatura está alta demais?
  • a resposta está criativa ou instável?
  • esse prompt está claro?
  • o modelo entendeu o papel que deve assumir?
  • o formato está bom para a aplicação?

Depois de encontrar uma configuração boa, aí sim faz sentido levá-la para o código.


2.3 O que é o Playground?

O Playground é um ambiente de testes onde você pode conversar com o modelo diretamente pela interface web, ajustando o comportamento da geração sem precisar programar naquele momento.

Na prática, ele funciona como uma área de experimentação.

Você pode:

  • escrever mensagens;
  • definir um contexto de sistema;
  • alterar parâmetros;
  • executar testes rapidamente;
  • comparar respostas;
  • descobrir o que funciona melhor antes de implementar no backend.

Por que isso é importante?

Porque a IA não é uma função matemática que sempre responde do mesmo jeito.

Ela é sensível a:

  • instrução;
  • contexto;
  • parâmetros;
  • exemplos fornecidos.

Por isso, testar em uma área controlada acelera muito o desenvolvimento.


2.4 O que observar ao usar o Playground?

Ao usar o Playground, você não deve testar de forma aleatória. O ideal é observar alguns pontos com método.

1. A resposta veio no formato esperado?

Se você queria apenas uma categoria, mas recebeu uma explicação longa, isso já é um sinal importante.

2. A resposta ficou estável?

Se o mesmo teste gera saídas muito diferentes, talvez a configuração esteja aberta demais.

3. A resposta ficou criativa demais?

Em tarefas técnicas, excesso de criatividade pode atrapalhar.

4. A resposta ficou curta demais?

Controle excessivo também pode gerar empobrecimento da resposta.

5. O resultado é útil para a aplicação?

Nem sempre uma resposta bonita é uma resposta utilizável pelo sistema.

Esse olhar é essencial porque, em aplicações, a pergunta correta não é apenas:

“A IA respondeu?”

Mas sim:

“A IA respondeu do jeito certo para o meu sistema usar?”


2.5 Entendendo os parâmetros da API

A API permite configurar alguns parâmetros que afetam diretamente o comportamento do modelo. Esses parâmetros ajudam a controlar aspectos como:

  • criatividade;
  • previsibilidade;
  • diversidade;
  • repetição;
  • tamanho da resposta.

2.6 Temperature: controlando criatividade e previsibilidade

A temperatura é um dos parâmetros mais conhecidos e mais importantes.

Ela controla o grau de aleatoriedade da resposta.

Explicando de forma simples

Podemos pensar assim:

  • temperatura baixa → resposta mais estável, previsível e conservadora;
  • temperatura alta → resposta mais criativa, mais variada e mais arriscada.

Exemplos práticos de interpretação

Temperatura baixa

Boa para:

  • categorização;
  • classificação;
  • respostas técnicas;
  • saídas padronizadas;
  • prompts que exigem precisão.

Temperatura mais alta

Boa para:

  • criatividade;
  • brainstorm;
  • nomes;
  • slogans;
  • textos mais livres.

Um alerta importante

Temperatura alta não significa automaticamente resposta melhor.

Em alguns casos, ela pode gerar resultados que fogem do objetivo da aplicação.

Faixa prática de leitura

De forma didática, você pode pensar assim:

  • 0.0 → extremamente controlada;
  • 0.5 a 0.7 → mais técnica;
  • 0.8 a 0.9 → equilíbrio interessante para várias tarefas;
  • 1.0 → padrão razoável em muitos cenários;
  • 1.2 ou mais → mais livre, com mais risco de instabilidade.

2.7 Max Tokens: controlando o tamanho da resposta

Outro parâmetro importante é o limite de tokens, que controla o tamanho máximo da resposta.

O que é um token?

Para fins de ebook, você pode entender token como uma unidade de texto processada pelo modelo. Nem sempre corresponde exatamente a uma palavra inteira, mas essa definição já é suficiente neste momento.

O que esse parâmetro faz?

Ele limita o quanto o modelo pode continuar escrevendo.

Quando isso é útil?

É útil quando sua aplicação precisa de respostas:

  • curtas;
  • objetivas;
  • dentro de um limite;
  • sem excesso de explicação.

2.8 TopP: controlando diversidade por amostragem

O parâmetro TopP também influencia diversidade, mas de uma forma diferente da temperatura.

Explicando de forma simples

Em vez de considerar todas as possibilidades para a próxima palavra, o modelo considera apenas um conjunto principal de opções mais prováveis.

O que isso significa na prática?

Quanto mais restrito esse conjunto, mais controlada tende a ser a escolha.

Dica didática

Na prática, muitos cenários começam ajustando primeiro a temperatura. TopP pode ser explorado depois, quando houver necessidade de ajuste mais fino.


2.9 Frequency Penalty e Presence Penalty

Esses dois parâmetros ajudam a controlar repetição.

Frequency Penalty

Reduz a chance de o modelo repetir demais palavras ou expressões que já apareceram na resposta.

Presence Penalty

Influencia a tendência de continuar reutilizando palavras ou temas já introduzidos no contexto.

Explicação didática

Para este início de curso, basta você entender:

  • ambos ajudam no controle de repetição;
  • podem ser úteis em textos mais longos;
  • geralmente não são o primeiro ajuste a ser feito em exemplos iniciais;
  • a temperatura costuma ser o primeiro parâmetro mais importante para aprender.

2.10 Stop Sequences: definindo onde a resposta deve parar

As sequências de parada servem para interromper a geração quando determinado padrão aparece.

Para que isso ajuda?

Ajuda quando queremos:

  • delimitar a saída;
  • impedir texto extra;
  • controlar melhor o final da resposta;
  • evitar que o modelo continue além do necessário.

2.11 O que o desenvolvedor deve aprender com esses parâmetros?

O ponto central não é decorar todos os nomes.

O que você precisa entender é:

a API oferece controles para moldar o comportamento do modelo.

Isso significa que o sistema não precisa aceitar qualquer saída como inevitável. Há espaço para ajuste.


2.12 Estudo de caso: triagem de chamados de suporte

Depois de entender Playground e parâmetros, um ótimo exemplo de uso é a triagem automática de chamados (help desk).

É um bom exemplo pedagógico porque mostra uma tarefa em que:

  • a IA não precisa ser super criativa;
  • a resposta precisa ser curta;
  • o formato precisa ser estável;
  • o backend ganha valor ao encaminhar filas sem análise manual linha a linha.

Cenário

Imagine um sistema em que cada chamado chega como texto livre. Em vez de rotular tudo à mão, a aplicação envia a descrição à IA e pede uma única etiqueta de encaminhamento.

Exemplos de descrição:

  • Não consigo redefinir minha senha — o link expira na hora;
  • O aplicativo trava ao salvar o formulário de cadastro;
  • Apareceu cobrança duplicada no meu cartão neste mês.

Nesse contexto, o comportamento desejado é mais conservador. Por isso, uma temperatura em torno de 0.85 costuma fazer sentido.


2.13 O papel da mensagem de sistema

A mensagem de sistema tem papel importante porque orienta o comportamento geral do modelo.

Exemplo simples de system

Você classifica chamados de suporte em uma única área.

Essa frase já muda bastante o comportamento do modelo.

Se o usuário mandar apenas:

Não consigo redefinir minha senha.

o modelo tende a interpretar aquilo como um relato a rotular, não como uma conversa aberta.


2.14 Levando o teste do Playground para o código

Depois de validar o comportamento no Playground, o próximo passo é reproduzir a ideia no projeto Java.

Exemplo

@GetMapping
public String triar(String descricao) {
    var system = "Você classifica chamados de suporte em uma única área.";

    return this.chatClient.prompt()
        .system(system)
        .user(descricao)
        .options(ChatOptionsBuilder.builder()
                .withTemperature(0.85f)
                .build())
        .call()
        .content();
}

2.15 Explicando essa implementação

String descricao

O texto do chamado é recebido dinamicamente pela URL. Isso torna a funcionalidade realmente utilizável.

var system

Define o papel do modelo.

.system(system)

Envia a instrução de sistema.

.user(descricao)

Envia o texto do chamado informado na requisição.

.options(...)

Permite ajustar o comportamento da chamada.

.withTemperature(0.85f)

Define uma temperatura mais apropriada para uma tarefa de triagem e rotulagem.

Como esses elementos se encaixam na chamada

flowchart TD URL["GET /triagem?descricao=Trava ao salvar"] --> P["String descricao = 'Trava ao salvar'"] P --> SYS["var system = 'classifica chamados...'"] SYS --> CHAIN["chatClient.prompt()"] CHAIN --> S[".system(system)papel do modelo"] S --> USR[".user(descricao)texto do chamado"] USR --> OPT[".options(...)temperatura, modelo, etc."] OPT --> CALL[".call()dispara a requisição"] CALL --> CONT[".content()extrai o texto"] CONT --> RESP["Resposta: 'Bug'"]

2.16 Tornando a chamada dinâmica

No começo, muitos exemplos usam valores fixos no código para facilitar o entendimento. Mas, para uma aplicação real, o ideal é receber a entrada dinamicamente.

Exemplo de chamada

GET /triagem?descricao=Cobran%20a%20duplicada%20no%20cartao

ou

GET /triagem?descricao=Link%20de%20reset%20de%20senha%20expira

Isso melhora bastante o valor didático do exemplo, porque Você enxerga a IA sendo aplicada a entradas reais.


2.17 O primeiro problema real: a resposta não está padronizada

Mesmo funcionando, a resposta ainda não está boa o suficiente para a aplicação.

Por exemplo, o modelo pode responder de maneiras diferentes dependendo do chamado:

  • em um caso, devolve apenas a categoria;
  • em outro, traz categoria e subcategoria;
  • em outro, vem com explicações adicionais.

Por que isso é um problema?

Porque o backend muitas vezes precisa de algo padronizado, como:

  • um único valor;
  • uma saída curta;
  • uma categoria entre opções válidas;
  • sem texto extra.

Se a resposta varia demais, isso dificulta:

  • persistência em banco;
  • comparação de resultados;
  • automações posteriores;
  • uso no front-end;
  • regras de negócio.

Aqui Você começa a perceber uma lição muito importante:

resposta boa para conversa não é, necessariamente, resposta boa para sistema.


2.18 Entrando em engenharia de prompt

É neste ponto que entra a engenharia de prompt.

Engenharia de prompt é o conjunto de técnicas usadas para orientar melhor o modelo, reduzindo ambiguidade e aumentando a chance de a resposta sair no formato esperado.

Na prática, isso significa deixar a instrução:

  • mais clara;
  • mais restrita;
  • mais específica;
  • mais alinhada ao caso de uso.

2.19 Do prompt genérico ao prompt específico

Versão genérica

Você classifica chamados de suporte.

Esse prompt ajuda, mas ainda deixa muita liberdade para o modelo decidir:

  • se vai explicar;
  • se vai detalhar demais;
  • se vai usar subcategorias;
  • se vai responder em frase completa.

Versão melhorada

Você classifica chamados de suporte e deve responder apenas o nome da área (uma linha).

Esse ajuste já reduz bastante a ambiguidade.


2.20 Refinando com lista fixa de categorias

Depois, o prompt pode evoluir ainda mais, passando a informar explicitamente quais categorias são válidas.

Exemplo

Escolha apenas uma etiqueta dentro da lista abaixo:
1. Acesso e autenticação
2. Bug ou estabilidade
3. Faturamento e cobrança
4. Outros

Por que isso ajuda?

Porque o modelo deixa de inventar rótulos fora do universo permitido pelo roteamento interno.

Se o sistema só encaminha para quatro filas, isso precisa estar claro no prompt.


2.21 Refinando com exemplo de uso

Outro ponto muito importante é a inclusão de exemplo de pergunta e resposta.

Exemplo

Chamado: O app fecha sozinho ao exportar PDF
Resposta: Bug ou estabilidade

Por que exemplos funcionam tão bem?

Porque eles ensinam ao modelo, de forma prática:

  • o formato da entrada;
  • o formato desejado da saída;
  • o nível de objetividade esperado;
  • o padrão que deve ser seguido.

2.22 Usando Text Block no Java

Para prompts maiores, o uso de Text Block no Java melhora muito a legibilidade.

Exemplo

String system = """
Você classifica chamados de suporte e deve responder apenas o nome da área (uma linha).

Escolha apenas uma etiqueta dentro da lista abaixo:
1. Acesso e autenticação
2. Bug ou estabilidade
3. Faturamento e cobrança
4. Outros

Exemplo de uso:
Chamado: O app fecha sozinho ao exportar PDF
Resposta: Bug ou estabilidade
""";

Vantagens

  • melhora leitura;
  • evita concatenação de strings;
  • facilita manutenção;
  • deixa o prompt visualmente mais claro.

2.23 Versão consolidada da triagem

@GetMapping
public String triar(String descricao) {
    var system = """
    Você classifica chamados de suporte e deve responder apenas o nome da área (uma linha).

    Escolha apenas uma etiqueta dentro da lista abaixo:
    1. Acesso e autenticação
    2. Bug ou estabilidade
    3. Faturamento e cobrança
    4. Outros

    Exemplo de uso:
    Chamado: O app fecha sozinho ao exportar PDF
    Resposta: Bug ou estabilidade
    """;

    return this.chatClient.prompt()
        .system(system)
        .user(descricao)
        .options(ChatOptionsBuilder.builder()
                .withTemperature(0.85f)
                .build())
        .call()
        .content();
}

2.24 O que você deve entender com esse exemplo?

Mais importante do que decorar a estrutura é compreender o raciocínio:

  1. a funcionalidade começou funcionando, mas com saída solta;
  2. percebemos que a aplicação precisava de mais controle;
  3. usamos Playground para entender comportamento;
  4. ajustamos temperatura;
  5. definimos melhor a mensagem de sistema;
  6. restringimos a lista de categorias;
  7. demos um exemplo de entrada e saída;
  8. a resposta ficou mais próxima do formato esperado.

Essa evolução mostra que desenvolver com IA é um processo de refinamento.


2.25 Boas práticas de prompt engineering

1. Diga qual papel o modelo deve assumir

Exemplo:

Você classifica chamados de suporte.

2. Diga exatamente o que ele deve devolver

Exemplo:

Responda apenas o nome da categoria

3. Restrinja o universo de resposta

Exemplo:

Escolha entre: Acesso e autenticação, Bug ou estabilidade, Faturamento e cobrança ou Outros

4. Forneça exemplos

Exemplos ajudam o modelo a reproduzir o padrão desejado.

5. Seja específico

Prompt vago gera saída vaga.

6. Ajuste a temperatura ao tipo de tarefa

Triagem e rotulagem pedem mais controle do que criatividade.

7. Pense no sistema, não só na conversa

Pergunte sempre: “Essa resposta é fácil para meu backend usar?”


2.26 Erros comuns nesta etapa

Erro 1: achar que a IA vai adivinhar o formato ideal

Não vai. Se você não disser claramente, a saída pode variar.

Erro 2: usar temperatura alta em tarefas técnicas

Isso pode aumentar a instabilidade sem necessidade.

Erro 3: não restringir etiquetas de fila

Se a aplicação encaminha apenas quatro filas, o prompt precisa refletir isso.

Erro 4: considerar qualquer resposta correta como suficiente

Para o sistema, formato importa.

Erro 5: testar pouco antes de implementar

O Playground existe justamente para evitar idas e vindas desnecessárias no backend.


2.27 Resumo do capítulo

Neste capítulo, você aprendeu que, depois de integrar com a IA, o próximo desafio é controlar melhor a resposta.

Vimos que:

  • o Playground ajuda a testar comportamento antes de alterar o código;
  • parâmetros como temperatura influenciam criatividade e previsibilidade;
  • o ChatClient permite enviar mensagens de sistema, mensagens do usuário e opções do modelo;
  • uma funcionalidade pode funcionar e, ainda assim, estar ruim para a aplicação se a resposta vier sem padrão;
  • engenharia de prompt ajuda a reduzir ambiguidade e aproximar a saída do formato esperado;
  • restringir etiquetas de fila e fornecer exemplos melhora bastante a previsibilidade da resposta.

2.28 Exercícios propostos

1. Conceitual
Explique com suas palavras por que o Playground pode economizar tempo no desenvolvimento com IA.

2. Conceitual
Qual a diferença entre uma tarefa que pede temperatura mais alta e outra que pede temperatura mais baixa?

3. Prático
Crie um prompt de sistema para classificar cursos em apenas estas categorias:

  • Programação
  • Design
  • Robótica
  • Outros

4. Prático
Reescreva o prompt abaixo para deixá-lo mais específico:

Você lê chamados de suporte

5. Reflexão
Por que uma resposta como “Categoria principal: Eletrônicos. Subcategorias possíveis...” pode ser ruim para um sistema automatizado?


Desafio extra

No Playground, monte um único prompt de sistema que force a saída em JSON com chaves categoria e confianca (0 a 1) para um texto de e‑mail de suporte. Depois, traduza a mesma ideia para um system + user no Java e valide se a resposta pode ser parseada com Jackson sem pós-processamento manual.


2.29 Fechamento

Agora você já entende que integrar com IA não é apenas fazer uma chamada e aceitar qualquer resposta. Em aplicações reais, o formato da saída importa tanto quanto o conteúdo.

Este capítulo foi importante porque mostrou a transição entre:

  • IA funcionando e
  • IA funcionando de forma útil para o sistema

Esse é um passo decisivo na sua evolução.

No próximo capítulo, fica natural avançar para temas como:

  • modelos;
  • tokens;
  • custo;
  • seleção de modelo;
  • troca dinâmica no Spring AI.

Capítulo 3 — Escolhendo o modelo certo: modelos, tokens, custo e troca dinâmica no Spring AI

Objetivo do capítulo

Neste capítulo, você vai aprender:

  • por que a escolha do modelo impacta custo, velocidade e qualidade da resposta;
  • como avaliar um modelo antes de adotá-lo na aplicação;
  • o que são tokens e por que eles influenciam limites e cobrança;
  • como trocar o modelo no Spring AI em diferentes níveis;
  • como configurar um modelo global para a aplicação;
  • como definir modelos diferentes para funcionalidades diferentes;
  • como estimar tokens para apoiar decisões técnicas;
  • como organizar melhor o código quando há mais de um ChatClient.

Ao final, a ideia é que você consiga responder a esta pergunta com segurança:

“Para esta funcionalidade, qual modelo faz mais sentido usar e onde devo configurar isso no código?”


3.1 Introdução

Nos capítulos anteriores, aprendemos a integrar a aplicação com IA, configurar parâmetros e melhorar prompts para tornar as respostas mais previsíveis.

Agora surge uma nova pergunta:

“Será que o modelo que estou usando é mesmo o melhor para esse caso?”

Essa pergunta é muito importante.

Quando uma aplicação usa IA, normalmente não existe um único modelo perfeito para tudo. Em alguns cenários, você quer:

  • mais qualidade em tarefas complexas;
  • menor custo em alto volume;
  • menor latência;
  • respostas mais longas;
  • melhor equilíbrio entre preço e desempenho.

Por isso, aprender a escolher modelo é parte do desenvolvimento com IA.


3.2 Por que a escolha do modelo importa?

Muita gente, no início, usa o primeiro modelo que funciona e segue em frente. Isso é normal. Mas, em projetos reais, essa decisão afeta diretamente o sistema.

O modelo influencia:

1. Qualidade da resposta
Modelos mais fortes tendem a lidar melhor com tarefas complexas.

2. Velocidade
Modelos menores ou mais enxutos podem responder mais rápido.

3. Custo
A cobrança varia conforme o modelo e a quantidade de tokens usada.

4. Limites de contexto e saída
Cada modelo possui limites de contexto e de tamanho máximo de resposta.

Conclusão prática

Escolher um modelo é uma decisão técnica e econômica ao mesmo tempo.

Não é apenas “qual responde melhor”, mas também:

  • qual cabe no orçamento;
  • qual responde no tempo certo;
  • qual atende ao tamanho do contexto;
  • qual faz sentido para a tarefa.

3.3 O catálogo de modelos muda com o tempo

Esse ponto é muito importante Para este ebook.

Os modelos disponíveis, as recomendações e até os preços podem mudar ao longo do tempo.

O que você precisa aprender com isso?

Ele não deve decorar um nome de modelo como se fosse uma verdade permanente.

O que ele precisa aprender é o critério de escolha:

  • preciso de mais capacidade ou mais economia?
  • meu volume é alto ou baixo?
  • a tarefa é simples ou complexa?
  • a resposta precisa ser longa?
  • a latência importa muito?

Esse raciocínio continua válido mesmo quando o catálogo muda.


3.4 O que observar ao comparar modelos?

Antes de configurar um modelo na aplicação, vale olhar quatro dimensões principais.

1. Capacidade

Alguns modelos são mais indicados para tarefas mais exigentes, como raciocínio mais complexo, código e fluxos mais avançados.

2. Latência

Modelos menores tendem a responder mais rápido.

3. Custo

Os preços por milhão de tokens variam bastante entre modelos.

4. Limites de contexto e saída

Cada modelo pode ter diferenças de janela de contexto e tamanho máximo de resposta.


3.5 Entendendo tokens de forma simples

Para escolher bem um modelo, você precisa entender o básico sobre tokens.

O que são tokens?

Tokens são unidades de texto usadas pelo modelo para processar entrada e saída. Eles funcionam como blocos menores que o sistema consegue manipular internamente.

De forma didática, você pode imaginar assim:

  • uma palavra pode virar um token;
  • uma palavra maior pode virar mais de um token;
  • números, símbolos e partes de palavras também podem virar tokens.

O importante, neste momento, é entender que o modelo não lê texto da mesma forma que uma pessoa lê. Ele processa tokens.


3.6 Por que tokens importam?

Tokens importam por três motivos principais.

1. Limite de contexto

Todo modelo tem um limite de quanto consegue considerar dentro da conversa ou da requisição.

2. Tamanho da resposta

Também existe limite para a quantidade de tokens que podem ser gerados na saída.

3. Custo

A cobrança da API é baseada em tokens.

Conclusão prática

Quanto mais tokens você usa, maior tende a ser:

  • o custo;
  • o tempo de processamento;
  • a necessidade de escolher um modelo com limite adequado.

3.7 Exemplo simples para visualizar tokens

Texto de entrada:

O sol está brilhando hoje.

De forma simplificada, o modelo pode quebrar isso em unidades menores para processamento.

A quantidade exata varia conforme a tokenização do modelo, mas a ideia principal é esta:

  1. o texto entra;
  2. ele é dividido em tokens;
  3. os tokens são processados;
  4. a resposta é gerada em novos tokens;
  5. esses tokens voltam a virar texto legível.

Para este ebook, o mais importante não é o cálculo exato neste momento, e sim o conceito de que entrada e saída consomem tokens.


3.8 Tokens, contexto e histórico da conversa

Quando usamos chat, o modelo não olha apenas a última mensagem. Em geral, ele considera um contexto composto por mensagens anteriores, instruções de sistema e conteúdo atual, até o limite da janela suportada pelo modelo.

Isso significa que, em aplicações maiores, devemos pensar em:

  • tamanho do histórico;
  • tamanho do system;
  • tamanho do user;
  • tamanho esperado da resposta.

Tudo isso entra na conta.


3.9 Tokens também influenciam custo

A cobrança por tokens é um dos pontos mais importantes para aplicações em escala.

O que isso ensina para você?

Se sua aplicação:

  • faz poucas chamadas;
  • atende poucos usuários;
  • usa prompts curtos;

o custo pode ser bem controlável.

Mas se ela:

  • roda em alto volume;
  • usa muito contexto;
  • gera respostas longas;
  • processa grandes lotes;

então escolher o modelo certo deixa de ser detalhe e vira decisão de arquitetura.


3.10 Quando faz sentido usar um modelo mais forte?

Use um modelo mais robusto quando a tarefa exigir:

  • raciocínio mais sofisticado;
  • melhor qualidade de resposta;
  • tarefas mais complexas;
  • maior exigência profissional;
  • workflows com mais nuance.

3.11 Quando faz sentido usar um modelo mais enxuto?

Use um modelo menor quando a prioridade for:

  • menor custo;
  • menor latência;
  • tarefas mais simples;
  • alto volume de chamadas;
  • classificações, rotulações ou respostas mais objetivas.

3.12 A pergunta certa não é “qual é o melhor modelo?”

Na prática, a pergunta certa é:

“Qual modelo é melhor para esta funcionalidade?”

Exemplos:

Gerador de rascunhos criativos (ex.: títulos, slogans)

Talvez você queira mais criatividade e qualidade de linguagem.

Triagem de chamados (rótulo único)

Talvez um modelo mais econômico já resolva muito bem.

Resumo técnico longo

Talvez você precise de melhor capacidade e mais tokens de saída.

Classificação em lote

Talvez a decisão passe muito mais por custo total do que por sofisticação máxima.

Essa mentalidade já ajuda Você a sair da ideia de “um modelo para tudo”.


3.13 Trocando o modelo no Spring AI: visão geral

O Spring AI é flexível para troca de modelo em diferentes níveis:

  • no prompt específico;
  • no ChatClient;
  • globalmente na aplicação.

Isso é ótimo do ponto de vista didático, porque mostra que a decisão pode ser feita no nível mais adequado ao caso de uso.


3.14 Troca de modelo no prompt específico

A forma mais localizada de trocar o modelo é configurar isso diretamente na chamada de um método específico.

Exemplo

public String triar(String descricao) {

    return this.chatClient.prompt()
            .system(system)
            .user(descricao)
            .options(ChatOptionsBuilder.builder()
                    .withTemperature(0.8f)
                    .withModel("gpt-4o-mini")
                    .build())
            .call()
            .content();
}

Quando isso é útil?

Quando você quer que apenas aquele prompt use um modelo diferente.

Vantagem

Alto controle local.

Desvantagem

Se vários métodos precisarem da mesma configuração, você acaba repetindo código.


3.15 Troca de modelo no ChatClient

Se vários métodos do mesmo controller ou serviço devem usar o mesmo modelo, faz mais sentido configurar o modelo no próprio ChatClient.

Exemplo

public TriagemController(ChatClient.Builder chatClientBuilder) {
    this.chatClient = chatClientBuilder
        .defaultOptions(ChatOptionsBuilder
                .builder()
                .withModel("gpt-4o-mini")
                .build())
        .build();
}

O que isso significa?

Todos os prompts criados a partir desse ChatClient herdarão esse modelo por padrão.

Quando isso é útil?

Quando uma funcionalidade inteira usa o mesmo perfil de modelo.


3.16 O mais específico sobrescreve o mais genérico

Esse é um ponto muito importante.

Em ordem de especificidade

  1. configuração no prompt específico;
  2. configuração no ChatClient;
  3. configuração global da aplicação.

Isso é muito útil porque permite:

  • ter um padrão global;
  • ter exceções por funcionalidade;
  • ter exceções ainda mais específicas por chamada.

3.17 Modelo padrão no Spring AI

Se você não definir explicitamente o modelo, o Spring AI usa valores padrão.

O que você precisa aprender aqui?

Se a aplicação funcionou sem você declarar explicitamente o modelo, isso não significa que “não existe modelo configurado”. Significa que alguma configuração padrão foi usada.


3.18 Configurando o modelo globalmente no application.properties

Quando você quer definir um modelo padrão para toda a aplicação, pode usar configuração global.

Exemplo

spring.ai.openai.chat.options.model=gpt-4o-mini

O que isso faz?

Todo ChatClient que não sobrescrever explicitamente o modelo passará a usar esse valor como padrão.

Quando isso é útil?

Quando o projeto inteiro ou a maior parte dele usa o mesmo modelo.


3.19 Qual nível escolher para configurar o modelo?

Aqui vai uma regra prática para você:

Use configuração global quando:

  • quase toda a aplicação usa o mesmo modelo.

Use configuração no ChatClient quando:

  • um grupo de funcionalidades compartilha o mesmo modelo.

Use configuração no prompt quando:

  • só uma chamada específica precisa de comportamento diferente.

Essa lógica ajuda a manter o projeto organizado e evita repetição.


3.20 Quando criar mais de um ChatClient?

Quando a aplicação cresce, é comum ter funcionalidades com perfis diferentes.

Exemplo:

  • um gerador de conteúdo pode pedir um modelo mais forte;
  • uma triagem de chamados pode pedir um modelo mais barato.

Nesse cenário, uma abordagem melhor é criar uma classe de configuração central e expor diferentes ChatClients como beans gerenciados pelo Spring, cada um com seu @Qualifier.

Exemplo

@Configuration
public class ChatClientConfiguration {

    @Bean
    @Qualifier("modelo-rapido")
    public ChatClient modeloRapidoChatClient(ChatClient.Builder chatClientBuilder) {
        return chatClientBuilder
            .defaultOptions(ChatOptionsBuilder
                    .builder()
                    .withModel("gpt-4o-mini")
                    .build())
            .build();
    }

    @Bean
    @Qualifier("modelo-capaz")
    public ChatClient modeloCapazChatClient(ChatClient.Builder chatClientBuilder) {
        return chatClientBuilder
            .defaultOptions(ChatOptionsBuilder
                    .builder()
                    .withModel("gpt-4o")
                    .build())
            .build();
    }
}

3.21 Injetando ChatClient por @Qualifier

Depois, cada controller recebe o cliente certo.

Exemplo

@RestController
@RequestMapping("/gerador")
public class GeradorController {

    private final ChatClient chatClient;

    public GeradorController(@Qualifier("modelo-capaz") ChatClient chatClient) {
        this.chatClient = chatClient;
    }
}
@RestController
@RequestMapping("/triagem")
public class TriagemController {

    private final ChatClient chatClient;

    public TriagemController(@Qualifier("modelo-rapido") ChatClient chatClient) {
        this.chatClient = chatClient;
    }
}

Essa estratégia é melhor do que replicar configuração manual em vários controllers.


3.22 Por que essa organização é melhor?

Porque ela traz:

  • centralização da configuração;
  • menos duplicação;
  • leitura mais clara;
  • manutenção mais fácil;
  • escalabilidade do projeto.

Em aplicações pequenas, configurar direto no controller pode até funcionar. Em aplicações maiores, isso começa a gerar confusão.


3.23 Contagem de tokens para apoiar escolha de modelo

Uma etapa mais avançada é usar uma biblioteca externa para contar tokens antes da chamada, ajudando na lógica de escolha de modelo.

Ideia prática

Antes de chamar a IA, você pode:

  1. contar tokens do system + user;
  2. estimar o tamanho do contexto;
  3. decidir qual modelo usar;
  4. evitar estourar limites;
  5. controlar custo.

3.24 Usando uma biblioteca para contar tokens

Uma biblioteca bastante usada para isso no Java é a JTokkit.

Exemplo de método

private int contarTokens(String system, String user) {
    var registry = Encodings.newDefaultEncodingRegistry();
    var enc = registry.getEncodingForModel(ModelType.GPT_4O_MINI);
    return enc.countTokens(system + user);
}

Depois:

var tokens = contarTokens(system, descricao);
System.out.println("QTD de tokens: " + tokens);

O que isso mostra?

Você passa a medir o tamanho aproximado da entrada antes de decidir qual modelo usar.


3.25 Onde essa lógica deveria ficar?

Para fins didáticos, pode até aparecer dentro do controller.
Mas arquiteturalmente, o ideal é mover essa responsabilidade para uma classe separada.

Melhor abordagem arquitetural

  • controller recebe a requisição;
  • serviço decide a estratégia;
  • componente de tokenização estima tokens;
  • componente de seleção escolhe o modelo;
  • ChatClient faz a chamada.

Assim, o projeto cresce de forma mais saudável.


3.26 Batch API: quando pensar em processamento em lote?

Quando a aplicação precisa processar muitas requisições que não exigem resposta imediata, vale pensar em processamento em lote.

Quando isso pode fazer sentido?

  • classificar milhares de registros;
  • gerar conteúdo em massa;
  • processar documentos;
  • produzir embeddings em lote;
  • tarefas offline ou não imediatas.

Quando não faz sentido?

  • endpoints síncronos que precisam responder imediatamente ao usuário.

3.27 O que você deve aprender de verdade neste capítulo?

Mais do que decorar propriedades ou nomes de modelos, ele precisa entender este raciocínio:

  1. diferentes funcionalidades podem exigir modelos diferentes;
  2. tokens influenciam limite e custo;
  3. custo e desempenho são parte da arquitetura;
  4. o Spring AI permite configurar o modelo em níveis diferentes;
  5. a configuração mais específica sobrescreve a mais genérica;
  6. em projetos maiores, centralizar ChatClients melhora bastante a organização.

3.28 Boas práticas desta etapa

1. Não escolha modelo por hábito

Escolha pelo caso de uso.

2. Evite um único modelo para tudo sem necessidade

Funcionalidades diferentes podem pedir perfis diferentes.

3. Use configuração global com critério

Ela é boa quando quase tudo segue o mesmo padrão.

4. Crie ChatClients específicos quando o sistema crescer

Isso ajuda muito na organização.

5. Observe tokens desde cedo

Mesmo em projetos pequenos, isso forma boa base técnica.

6. Separe lógica de seleção de modelo do controller

O controller deve ficar simples.

7. Revise documentação periodicamente

Modelos, defaults e preços mudam com o tempo.


3.29 Erros comuns nesta etapa

Erro 1: usar sempre o mesmo modelo sem avaliar a tarefa

Isso pode gerar custo desnecessário ou qualidade insuficiente.

Erro 2: ignorar tokens

Quando o sistema cresce, isso cobra seu preço.

Erro 3: repetir withModel(...) em todo lugar

Isso dificulta manutenção.

Erro 4: misturar decisão de modelo diretamente no controller

Em aplicações maiores, isso suja a camada web.

Erro 5: decorar preços e nomes como se fossem fixos

Essas informações mudam. O importante é entender o critério de escolha.


3.30 Resumo do capítulo

Neste capítulo, você aprendeu que escolher o modelo certo é parte essencial do desenvolvimento com IA.

Vimos que:

  • os modelos diferem em capacidade, custo, latência e limites de contexto/saída;
  • tokens são a base de processamento do texto e influenciam tanto limites quanto cobrança;
  • o Spring AI permite configurar o modelo no prompt, no ChatClient ou globalmente por propriedades;
  • a configuração mais específica sobrescreve a mais genérica;
  • em aplicações com mais de uma necessidade, vale criar múltiplos ChatClients e injetá-los com @Qualifier;
  • uma biblioteca de contagem de tokens pode ajudar na troca dinâmica de modelo;
  • processamento em lote faz sentido em cenários assíncronos e de alto volume.

3.31 Exercícios propostos

1. Conceitual
Explique com suas palavras por que nem sempre vale usar o mesmo modelo para todas as funcionalidades.

2. Conceitual
O que são tokens e por que eles influenciam custo e limite da aplicação?

3. Prático
Mostre um exemplo de quando faz sentido configurar o modelo:

  • globalmente;
  • no ChatClient;
  • no prompt específico.

4. Prático
Descreva um cenário em que o gerador de conteúdo use um modelo mais forte e a triagem de chamados use um modelo mais econômico.

5. Arquitetura
Por que criar uma classe de configuração com múltiplos ChatClients pode ser melhor do que repetir withModel(...) em vários controllers?


Desafio extra

Simule dois beans ChatClient (ex.: chatClientRapido e chatClientPreciso) apontando para modelos diferentes no application.properties. Exponha dois endpoints que respondam à mesma pergunta e compare latência e qualidade em 3 perguntas reais (anote tempos com System.nanoTime() ou logs).


3.32 Fechamento

Com este capítulo, você deixa de ver o modelo como um detalhe fixo e passa a entendê-lo como uma decisão de arquitetura.

Esse é um avanço importante, porque agora você começa a pensar em IA com mais maturidade:

  • não só “funciona?”;
  • mas também “vale a pena?”;
  • “custa quanto?”;
  • “escala bem?”;
  • “faz sentido para esta funcionalidade?”.

Esse tipo de pensamento aproxima muito mais o uso da IA de cenários reais de desenvolvimento.


Capítulo 4 — Lidando com erros, retentativas e logs em integrações com Spring AI

Objetivo do capítulo

Neste capítulo, você vai aprender:

  • por que toda integração com API externa precisa considerar falhas;
  • quais erros podem acontecer ao chamar a OpenAI;
  • o que significam códigos como 401, 403, 429, 500 e 503;
  • por que nem todo erro deve ser tratado da mesma forma;
  • como o Spring AI já faz retentativas automáticas;
  • como personalizar o comportamento de retry no application.properties;
  • quando vale a pena deixar o Spring tentar de novo e quando não vale;
  • como registrar logs da integração com SimpleLoggerAdvisor;
  • quais cuidados tomar para não vazar dados sensíveis nos logs.

Ao final, você deve entender algo essencial:

uma integração profissional com IA não depende só de prompt e modelo; ela depende também de resiliência, previsibilidade e observabilidade.


4.1 Introdução

Até aqui, nossa aplicação chamou a API da OpenAI e recebeu respostas com sucesso. Isso é ótimo para aprendizagem inicial, mas existe uma realidade que todo desenvolvedor precisa aceitar:

quando sua aplicação conversa com um serviço externo, falhas fazem parte do cenário.

No nosso projeto, quando executamos algo como:

return this.chatClient.prompt()
        .system(system)
        .user(descricao)
        .options(ChatOptionsBuilder.builder()
                .withTemperature(0.8f)
                .build())
        .call()
        .content();

a execução sai do nosso sistema e depende de outra infraestrutura.

Isso significa que podem acontecer falhas por vários motivos:

  • erro nosso de autenticação;
  • uso incorreto da API;
  • excesso de requisições;
  • instabilidade de rede;
  • indisponibilidade do provedor;
  • sobrecarga temporária do serviço;
  • problemas de configuração no projeto.

Portanto, um backend que usa IA precisa ser preparado não apenas para o cenário de sucesso, mas também para o cenário de falha.


4.2 Por que esse capítulo é tão importante?

Nos primeiros exemplos, é fácil acreditar que a API estará sempre disponível. Mas, em aplicações reais, isso não é verdade.

Se sua aplicação depende de um serviço externo e você não pensa em falhas, pode acabar com problemas como:

  • endpoints quebrando sem tratamento claro;
  • usuários vendo erro técnico bruto;
  • loops excessivos de requisição;
  • aumento desnecessário de custo;
  • dificuldade para descobrir o que aconteceu;
  • ausência de logs para auditoria;
  • comportamento inconsistente em produção.

Em outras palavras: fazer a integração funcionar é só a primeira metade do trabalho. A segunda metade é garantir que ela continue utilizável quando as coisas não saírem como o esperado.


4.3 Entendendo o cenário de falha em APIs externas

Quando sua aplicação chama um serviço interno da própria empresa, às vezes você ainda tem controle sobre logs, infraestrutura e debugging. Mas, ao chamar uma API externa, parte do processamento fica fora do seu domínio.

Você não controla diretamente:

  • a disponibilidade do serviço externo;
  • a latência daquele servidor;
  • a fila interna do provedor;
  • o rate limit;
  • a política de segurança da conta;
  • incidentes temporários da plataforma.

Por isso, todo consumo de API externa precisa ser pensado como uma comunicação sujeita a falhas.

Essa mentalidade muda tudo. O desenvolvedor deixa de pensar apenas em “como chamar” e passa a pensar também em:

  • o que pode dar errado;
  • como o sistema deve reagir;
  • quando tentar novamente;
  • quando registrar e parar;
  • o que mostrar ao usuário;
  • o que monitorar.

4.4 Que tipos de erro podem acontecer?

APIs como a da OpenAI usam códigos HTTP para indicar o tipo de falha.

Entre os erros mais comuns estão:

  • 401 para problemas de autenticação;
  • 403 para restrição de acesso;
  • 429 para limite excedido;
  • 500 para erro interno do servidor;
  • 503 para indisponibilidade temporária.

Vamos entender cada um de forma didática.


4.5 Erro 401 — autenticação inválida

O erro 401 aparece quando a autenticação falha.

O que isso significa na prática?

A aplicação tentou usar a API, mas não conseguiu provar que tinha permissão para isso.

Causas comuns

  • variável de ambiente não carregada;
  • chave digitada errada;
  • chave revogada;
  • chave pertencente a outro projeto;
  • configuração incorreta de organização ou projeto.

Exemplo de impacto

Se a aplicação subir sem a chave correta, a chamada ao modelo falhará.

Esse não é um erro para “tentar de novo sem mudar nada”.
Primeiro é preciso corrigir a credencial.

O que você precisa aprender aqui?

Nem todo erro temporário justifica retry.
Se o problema é autenticação, repetir a mesma requisição 10 vezes com a mesma chave inválida não resolve.


4.6 Erro 403 — acesso negado

O erro 403 indica que a requisição foi compreendida, mas o acesso não é permitido.

Em linguagem simples

A aplicação conseguiu falar com a API, mas a API recusou o uso por restrição de permissão ou política.

O que isso ensina?

Esse também não costuma ser um erro que se resolve apenas repetindo a chamada.
É mais um caso de configuração, política de conta ou restrição do ambiente.


4.7 Erro 429 — limite excedido

O erro 429 é um dos mais importantes em integrações reais.

O que isso quer dizer?

Sua aplicação tentou usar a API mais rápido do que o limite permite, ou ultrapassou a capacidade disponível para aquela conta/projeto.

Situações comuns

  • muitas requisições em loop;
  • várias chamadas concorrentes sem controle;
  • batch improvisado via múltiplas chamadas síncronas;
  • aumento repentino de tráfego;
  • falta de limitação do lado da aplicação;
  • saldo ou limite de gasto esgotado.

Por que esse erro é crítico?

Porque ele costuma aparecer justamente quando o sistema começa a escalar.

Em ambiente de teste, talvez você nunca veja 429.
Em ambiente real, ele pode surgir rapidamente se não houver controle.

O que fazer?

  • reduzir a taxa de chamadas;
  • evitar chamadas redundantes;
  • repensar concorrência;
  • avaliar batching ou fila;
  • monitorar volume e consumo.

Esse é um bom exemplo de erro que pode justificar retentativa com backoff, desde que o problema seja momentâneo e não estrutural.


4.8 Erro 500 — falha interna do servidor

O erro 500 significa que houve um problema no lado do provedor.

Em linguagem simples

A falha não é, necessariamente, da sua aplicação. O servidor da API pode estar com problema interno.

O que isso ensina?

Aqui sim a ideia de retry faz muito sentido.

Se a falha foi transitória, uma nova tentativa após alguns segundos pode resolver.


4.9 Erro 503 — sobrecarga ou indisponibilidade temporária

O erro 503 indica indisponibilidade temporária ou sobrecarga do serviço.

O que isso quer dizer?

O serviço está momentaneamente incapaz de atender no ritmo atual.

Esse é um erro clássico de integração resiliente

Não é, necessariamente, culpa do seu código.
Mas seu código precisa reagir bem a ele.


4.10 Nem todo erro deve ser tratado do mesmo jeito

Esse é um dos pontos mais importantes do capítulo.

Um erro 401 e um erro 500 são muito diferentes.

  • 401 geralmente pede correção de configuração;
  • 429 pode pedir desaceleração;
  • 500/503 pedem tolerância a falha temporária.

Por isso, o desenvolvedor não deve pensar:

“Qualquer erro eu tento de novo.”

A pergunta correta é:

“Esse erro parece transitório ou estrutural?”

Erro estrutural

Algo está errado na configuração, autenticação ou permissão.

Erro transitório

Algo temporário aconteceu na infraestrutura, latência ou carga.

Essa distinção é essencial para construir um sistema maduro.


4.11 O que seria um tratamento ingênuo?

Uma primeira reação comum é envolver a chamada com try-catch e tratar tudo manualmente.

Exemplo conceitual:

try {
    return this.chatClient.prompt()
            .system(system)
            .user(descricao)
            .call()
            .content();
}
catch (Exception e) {
    // tratar erro
}

Por que um try-catch genérico não resolve sozinho?

Porque ele:

  • não distingue bem os tipos de falha;
  • pode esconder o erro real;
  • pode duplicar trabalho já feito pelo framework;
  • pode espalhar lógica de tratamento em vários controllers;
  • não resolve sozinho a estratégia de retry.

4.12 O papel do Spring AI no tratamento de falhas

A boa notícia é que o Spring AI já oferece mecanismo de retry configurável.

Em termos práticos

Se ocorrer uma falha temporária, o framework pode:

  • tentar novamente;
  • esperar um intervalo;
  • aplicar backoff;
  • insistir até um limite;
  • só depois lançar exceção.

Isso reduz a necessidade de implementar esse comportamento manualmente em cada endpoint.


4.13 O que é retry?

Retry é a estratégia de tentar novamente uma operação que falhou, supondo que aquela falha possa ser temporária.

Exemplo simples

A aplicação chama a API.
A API responde com falha momentânea.
Em vez de desistir imediatamente, o sistema espera um pouco e tenta de novo.

Quando isso é útil?

Quando o erro pode desaparecer em poucos segundos, como em:

  • instabilidade de rede;
  • timeout temporário;
  • sobrecarga do servidor;
  • falha transitória do provedor.

4.14 O que é backoff exponencial?

A ideia de aumentar o tempo de espera a cada nova tentativa corresponde ao backoff exponencial.

Explicando de forma simples

Em vez de repetir assim:

  • espera 2 segundos;
  • espera 2 segundos;
  • espera 2 segundos;

o sistema pode fazer algo como:

  • espera 2 segundos;
  • depois espera mais;
  • depois espera ainda mais;
  • e assim por diante, até um limite.

Por que isso é melhor?

Porque, se o serviço está sobrecarregado, insistir rápido demais pode piorar o problema.

O backoff ajuda a:

  • reduzir pressão sobre a API;
  • dar tempo para recuperação do serviço;
  • evitar tempestade de retentativas;
  • melhorar comportamento sob falha.

4.15 Entendendo as propriedades de retry do Spring AI

O Spring AI expõe propriedades específicas para controlar esse comportamento.

spring.ai.retry.max-attempts

Define o número máximo de tentativas.

spring.ai.retry.backoff.initial-interval

Define o intervalo inicial antes da próxima tentativa.

spring.ai.retry.backoff.multiplier

Define o multiplicador do backoff.

spring.ai.retry.backoff.max-interval

Define o intervalo máximo entre tentativas.


4.16 O que esses valores significam na prática?

Eles definem a política de tolerância a falhas da sua aplicação.

Exemplo conceitual

Se uma chamada falhar:

  • o Spring não desiste imediatamente;
  • ele tenta novamente;
  • espera um intervalo;
  • aplica backoff;
  • continua até atingir o máximo configurado;
  • se mesmo assim não funcionar, lança exceção.

Isso torna o sistema mais resiliente a falhas temporárias.


4.17 Personalizando o retry no application.properties

Você pode sobrescrever os padrões do framework no application.properties.

Exemplo didático

spring.ai.retry.max-attempts=20
spring.ai.retry.backoff.initial-interval=10
spring.ai.retry.backoff.max-interval=300

Quando personalizar faz sentido?

  • quando seu sistema suporta esperar mais;
  • quando a operação é importante e vale insistir mais;
  • quando a latência extra é aceitável;
  • quando a API externa tem instabilidades frequentes e curtas.

Quando não exagerar?

Se você aumentar demais o retry, pode acabar:

  • tornando a resposta muito lenta;
  • segurando threads por tempo demais;
  • piorando a experiência do usuário;
  • mascarando incidentes mais sérios.

4.18 Retry resolve tudo?

Não.

Esse ponto precisa ficar muito claro para você.

Retry ajuda principalmente em falhas transitórias, não em falhas estruturais.

Retry ajuda em:

  • indisponibilidade momentânea;
  • sobrecarga temporária;
  • oscilações de rede;
  • timeout passageiro.

Retry não resolve sozinho:

  • chave inválida;
  • projeto sem permissão;
  • configuração incorreta;
  • região não suportada;
  • lógica ruim de chamadas em excesso;
  • falta de saldo/quota.

Ou seja, retry é uma ferramenta importante, mas não substitui boa arquitetura e boa operação.


4.19 O que mostrar para o usuário quando der erro?

Nem sempre faz sentido devolver o erro bruto da API para o usuário final.

O que evitar

  • stack trace na tela;
  • mensagem técnica exposta diretamente;
  • texto interno do framework para o cliente;
  • informação sensível no retorno.

O ideal

Em muitas aplicações, o usuário deve receber algo mais controlado, como:

  • “Não foi possível processar sua solicitação agora.”
  • “Tente novamente em instantes.”
  • “Serviço temporariamente indisponível.”

Enquanto isso, os detalhes técnicos ficam registrados nos logs.


4.20 Onde tratar erros na aplicação?

Espalhar lógica de erro em todos os controllers não costuma ser a melhor abordagem.

Por que isso é importante?

Porque tratamento duplicado em controller tende a gerar:

  • repetição;
  • inconsistência;
  • manutenção difícil;
  • mistura de responsabilidade.

Melhor abordagem didática

O controller deve focar em:

  • receber a requisição;
  • chamar a regra de negócio;
  • devolver o resultado.

O tratamento centralizado de exceções ajuda a manter o projeto mais limpo.


4.21 Erro tratado não basta: precisamos de observabilidade

Até aqui, falamos em falhar e tentar novamente. Mas existe outra pergunta importante:

quando algo acontece, como eu descubro o que realmente foi enviado e o que voltou?

Aí entra o tema de logs e observabilidade.

Sem logs adequados, você pode não saber:

  • qual prompt foi enviado;
  • qual modelo foi usado;
  • qual temperatura estava configurada;
  • quantos tokens entraram ou saíram;
  • qual foi o retorno real da API;
  • por que a chamada falhou.

4.22 O que é um advisor no Spring AI?

Um advisor funciona como um interceptador da conversa entre sua aplicação e o modelo.

Ele pode observar:

  • o que está sendo enviado;
  • o que está sendo recebido;
  • o comportamento da chamada.

4.23 SimpleLoggerAdvisor: logando requisição e resposta

Uma forma simples de registrar request e response é usar o SimpleLoggerAdvisor.

Exemplo didático

return this.chatClient.prompt()
    .system(system)
    .user(descricao)
    .options(ChatOptionsBuilder.builder()
        .withTemperature(0.85f)
        .build())
    .advisors(new SimpleLoggerAdvisor())
    .call()
    .content();

O que isso traz?

Permite registrar:

  • modelo;
  • temperatura;
  • prompt de sistema;
  • prompt do usuário;
  • resposta;
  • metadados úteis para análise.

4.24 Habilitando o nível de log no application.properties

Para visualizar esses logs, é preciso ajustar o nível de log da categoria correspondente.

Exemplo

logging.level.org.springframework.ai.chat.client.advisor=DEBUG

Com isso, os logs produzidos pelo advisor passam a aparecer no console.


4.25 O que esses logs ajudam a responder?

Com o logging ativado, você consegue entender melhor perguntas como:

  • o modelo realmente usado foi o esperado?
  • a temperatura era a correta?
  • o prompt de sistema estava certo?
  • o usuário enviou o texto que imaginávamos?
  • a resposta foi coerente?
  • houve algum metadado importante retornado?

4.26 Logs ajudam em quatro frentes

1. Debug

Você entende por que a resposta veio daquele jeito.

2. Auditoria

Você consegue rastrear o comportamento da integração.

3. Observabilidade

Você passa a ter visibilidade sobre o que o sistema está fazendo.

4. Melhoria contínua

Você encontra padrões, ajusta prompts e descobre problemas recorrentes.


4.27 Cuidado: log em excesso também é risco

Registrar tudo pode ser perigoso se houver dados sensíveis.

O que pode ser sensível?

  • dados pessoais;
  • CPF;
  • e-mail;
  • telefone;
  • conteúdo privado;
  • informações de negócio restritas;
  • chaves ou tokens;
  • prompts com dados internos.

Boa prática

Em desenvolvimento, logs detalhados ajudam muito.
Em produção, é preciso avaliar com cuidado:

  • o que será logado;
  • quem terá acesso;
  • por quanto tempo;
  • se haverá mascaramento;
  • se aquele log é mesmo necessário.

4.28 Resiliência + observabilidade = integração madura

Uma integração madura com IA precisa de dois pilares:

Resiliência

Capacidade de continuar operando ou se recuperar diante de falhas temporárias.

Observabilidade

Capacidade de entender o que aconteceu, por que aconteceu e como corrigir.

Se você tem só resiliência e não tem logs, o sistema até tenta se recuperar, mas você não sabe o que está acontecendo.
Se você tem só logs e não tem retry, você até enxerga a falha, mas o sistema quebra com facilidade.

Os dois juntos elevam bastante a qualidade da aplicação.


4.29 O que você deve guardar deste capítulo?

Mais do que decorar propriedades, ele precisa entender estas ideias:

  1. chamar API externa implica risco de falha;
  2. falha faz parte da arquitetura, não é exceção rara;
  3. erros diferentes exigem reações diferentes;
  4. retry ajuda em falhas temporárias;
  5. o Spring AI já oferece política de retentativa;
  6. essa política pode ser personalizada;
  7. logs são fundamentais para debugar e operar;
  8. logs precisam ser usados com responsabilidade.

4.30 Boas práticas desta etapa

1. Nunca assuma disponibilidade total da API

Serviços externos falham.

2. Diferencie erro transitório de erro estrutural

Nem toda falha deve ser tratada com retry.

3. Use os retries do framework antes de reinventar a roda

O Spring AI já traz suporte a isso.

4. Ajuste retry com critério

Mais tentativas nem sempre significam melhor experiência.

5. Centralize tratamento de erro quando fizer sentido

Evite duplicação em controller.

6. Logue o suficiente para investigar

Mas não transforme log em vazamento de dados.

7. Observe custo indireto de falhas

Retries também consomem tempo e, dependendo do caso, podem consumir chamadas.


4.31 Erros comuns nesta etapa

Erro 1: achar que a API sempre estará disponível

Não estará.

Erro 2: tentar resolver tudo com try-catch genérico

Isso pode esconder problema e gerar código espalhado.

Erro 3: tratar 401 como se fosse falha temporária

Se a chave está errada, insistir não resolve.

Erro 4: não monitorar 429

Esse erro costuma aparecer quando a aplicação começa a crescer.

Erro 5: ativar log detalhado em produção sem cuidado

Isso pode expor dados indevidos.

Erro 6: não registrar contexto suficiente

Sem contexto, o debug vira adivinhação.


4.32 Exemplo de configuração consolidada

application.properties

spring.ai.retry.max-attempts=20
spring.ai.retry.backoff.initial-interval=10
spring.ai.retry.backoff.max-interval=300

logging.level.org.springframework.ai.chat.client.advisor=DEBUG

Uso do advisor no prompt

return this.chatClient.prompt()
    .system(system)
    .user(descricao)
    .options(ChatOptionsBuilder.builder()
        .withTemperature(0.85f)
        .build())
    .advisors(new SimpleLoggerAdvisor())
    .call()
    .content();

Esse conjunto mostra, de forma prática, duas frentes do capítulo:

  • tolerância a falha;
  • visibilidade da integração.

4.33 Resumo do capítulo

Neste capítulo, você aprendeu que uma integração com IA precisa ser preparada para falhas e para análise operacional.

Vimos que:

  • APIs externas podem devolver erros diferentes, cada um com significado próprio;
  • erros de autenticação e permissão não devem ser tratados como simples falhas temporárias;
  • o Spring AI já oferece mecanismo de retry configurável via propriedades;
  • é possível sobrescrever esses valores no application.properties;
  • o SimpleLoggerAdvisor ajuda a registrar request e response;
  • para visualizar esses logs, é preciso configurar o nível de log apropriado;
  • logs ajudam muito em debug, auditoria e observabilidade, mas exigem cuidado com dados sensíveis.

4.34 Exercícios propostos

1. Conceitual
Explique com suas palavras por que uma API externa não deve ser tratada como se estivesse sempre disponível.

2. Conceitual
Qual a diferença entre um erro 401 e um erro 500?

3. Aplicação prática
Descreva um cenário em que retry faz sentido e outro em que retry não resolve o problema.

4. Configuração
Mostre como alterar no application.properties:

  • quantidade máxima de tentativas;
  • intervalo inicial;
  • intervalo máximo.

5. Observabilidade
Explique por que logs detalhados ajudam no debug, mas podem ser perigosos em produção.


Desafio extra

Configure spring.ai.retry.* com valores baixos e force um erro 503 (ou simule indisponibilidade). Depois, aumente max-attempts e meça quantas linhas de log o SimpleLoggerAdvisor gera por requisição. Descreva em uma tabela: cenário → número de tentativas → risco de poluir log em produção.


4.35 Fechamento

Com este capítulo, você começa a dar um passo importante em direção a aplicações mais profissionais.

Agora você não pensa apenas em:

  • “como chamar a IA?”,

mas também em:

  • “o que acontece quando falha?”,
  • “como meu sistema reage?”,
  • “como descubro o que aconteceu?”,
  • “como manter isso utilizável em produção?”.

Esse é um capítulo-chave porque muda a maturidade do projeto.


Capítulo 5 — Multimodalidade no Spring AI: geração de imagens e troca de provedores sem reescrever a aplicação

Objetivo do capítulo

Neste capítulo, você vai aprender:

  • o que significa multimodalidade em aplicações com IA;
  • por que a IA generativa não se limita apenas ao modo chat;
  • como gerar imagens em uma aplicação Spring Boot usando Spring AI;
  • quais classes do Spring AI são usadas no fluxo de geração de imagens;
  • como funciona a chamada com ImageModel;
  • como retornar a URL de uma imagem gerada;
  • por que o código fica desacoplado do provedor quando usamos as abstrações do Spring;
  • qual é o impacto real de trocar OpenAI por outro provedor suportado.

Ao final, você deve entender algo muito importante:

quando usamos corretamente as abstrações do Spring AI, a aplicação fica mais flexível, mais reaproveitável e menos presa a um único fornecedor.


5.1 Introdução

Até este ponto do curso, nosso foco principal foi o uso de IA para geração de texto. Isso faz sentido, porque o chat costuma ser a porta de entrada mais comum para quem começa a trabalhar com inteligência artificial em aplicações.

Mas os provedores atuais de IA vão além do texto.

Hoje, muitos deles oferecem modelos capazes de trabalhar com diferentes tipos de mídia, como:

  • texto;
  • imagem;
  • áudio;
  • fala;
  • transcrição;
  • e, em alguns contextos, vídeo.

Isso significa que, ao construir uma aplicação com Spring AI, você não está limitado a um chatbot. Você pode enriquecer o sistema com outras capacidades de IA, desde que isso faça sentido para o problema que está sendo resolvido.

Este capítulo mostra exatamente esse movimento: depois de trabalhar bastante com chat, agora vamos olhar para a geração de imagens e, ao mesmo tempo, entender uma das maiores vantagens arquiteturais do Spring AI: a facilidade para trocar de provedor sem reescrever a aplicação inteira.


5.2 O que é multimodalidade?

Multimodalidade é a capacidade de um sistema de IA de lidar com diferentes tipos de entrada e saída, em vez de trabalhar apenas com texto.

Exemplo simples

Um sistema pode:

  • receber texto e gerar texto;
  • receber texto e gerar imagem;
  • receber áudio e gerar transcrição;
  • receber texto e gerar fala;
  • combinar diferentes tipos de mídia.

Por que isso é importante?

Porque amplia muito o tipo de aplicação que podemos construir.

Exemplos:

  • comunicação institucional com artes ou ilustrações geradas sob demanda;
  • sistema educacional com criação de ilustrações;
  • chatbot com transcrição de áudio;
  • assistente que transforma texto em fala;
  • ferramenta que analisa conteúdo multimídia.

Ou seja, IA em software não é só conversa. É também conteúdo multimídia.


5.3 Por que esse tema importa para este ebook?

Esse capítulo é importante porque ajuda você a expandir a visão.

Até aqui, ele pode estar pensando:

“Spring AI serve para fazer perguntas ao modelo e receber texto.”

Mas isso seria uma visão limitada.

O Spring AI foi desenhado para oferecer abstrações específicas para diferentes tipos de modelos, e a geração de imagem é um ótimo exemplo disso.

Então este capítulo serve para mostrar duas coisas:

  1. o framework suporta outros modos além de chat;
  2. o desacoplamento oferecido pelo Spring AI se torna ainda mais valioso quando pensamos em múltiplos provedores e múltiplos tipos de modelo.

5.4 Gerar imagem não é a mesma coisa que gerar texto

Esse ponto precisa ficar muito claro para você.

No modo chat, usamos classes como:

  • ChatClient
  • mensagens de sistema e usuário
  • opções de chat
  • retorno textual

Na geração de imagem, o fluxo muda.

Em vez de trabalhar com um cliente de chat, passamos a trabalhar com uma abstração específica para imagem.

O que isso ensina?

Cada tipo de capacidade de IA tem suas próprias classes e seu próprio fluxo, mesmo que a filosofia geral continue parecida:

  • há uma abstração do Spring;
  • há um prompt ou request;
  • há opções;
  • há um call();
  • há um objeto de resposta.

Isso torna o aprendizado mais consistente.


5.5 Visão geral da API de imagem no Spring AI

Para entender o fluxo de geração de imagem, você precisa conhecer estas classes:

  • ImageModel
  • ImagePrompt
  • ImageResponse
  • ImageOptionsBuilder

Em linguagem simples

ImageModel
É o componente que conversa com o modelo de geração de imagem.

ImagePrompt
É o objeto que encapsula a solicitação de geração.

ImageOptionsBuilder
Permite definir opções como largura e altura.

ImageResponse
É a resposta gerada pela API após a criação da imagem.


5.6 Criando um controller para geração de imagem

Uma boa prática é criar um controller dedicado para essa funcionalidade.

Estrutura inicial

@RestController
@RequestMapping("imagem")
public class GeradorDeImagensController {

    @GetMapping
    public String gerarImagem(String prompt) {

    }
}

O que esse controller faz?

Ele cria uma nova rota dedicada à geração de imagens.
O usuário envia um prompt textual, e a aplicação usa esse texto para pedir que a IA gere uma imagem correspondente.


5.7 Injetando o ImageModel

Diferente do ChatClient, o ImageModel pode ser injetado diretamente.

Exemplo

import org.springframework.ai.image.ImageModel;

@RestController
@RequestMapping("imagem")
public class GeradorDeImagensController {

    private final ImageModel imageModel;

    public GeradorDeImagensController(ImageModel imageModel) {
        this.imageModel = imageModel;
    }
}

O que isso ensina?

O Spring já consegue disponibilizar a implementação apropriada do modelo de imagem.

Isso mantém o estilo de desenvolvimento consistente:

  • você injeta uma abstração do Spring;
  • usa essa abstração no controller ou serviço;
  • não precisa acoplar o código diretamente ao provedor.

5.8 Montando as opções da imagem

Antes de chamar a geração, precisamos definir as opções da imagem.

Exemplo

var options = ImageOptionsBuilder
        .builder()
        .withHeight(1024)
        .withWidth(1024)
        .build();

O que isso significa?

Estamos pedindo que a imagem seja gerada com:

  • altura: 1024 pixels
  • largura: 1024 pixels

Por que isso importa?

Porque geração de imagem também envolve decisão de formato.

Em aplicações reais, você pode querer:

  • imagem quadrada para redes sociais;
  • imagem horizontal para banner;
  • imagem menor para economizar custo e tempo;
  • imagem maior para material promocional.

Mesmo em um exemplo simples, já vale mostrar para você que essas decisões fazem parte da implementação.


5.9 Criando o ImagePrompt

Depois das opções, criamos o prompt da imagem.

Exemplo

new ImagePrompt(prompt, options)

O que isso faz?

Esse objeto encapsula duas coisas:

  • a instrução textual dada pelo usuário;
  • as opções da imagem.

Exemplo de instrução:

Paisagem ao amanhecer com montanhas suaves e névoa leve, estilo ilustração digital

Ou seja, a IA vai tentar gerar uma imagem coerente com essa descrição.


5.10 Chamando o modelo de imagem

Agora chegamos ao núcleo da funcionalidade.

Exemplo

var response = imageModel.call(new ImagePrompt(prompt, options));

O que está acontecendo aqui?

  1. o usuário envia o texto;
  2. o backend cria o ImagePrompt;
  3. o ImageModel chama a API do provedor;
  4. o provedor gera a imagem;
  5. a resposta volta para a aplicação.

Perceba que a ideia geral lembra o modo chat, mas com classes específicas para imagem.


5.11 Extraindo a URL da imagem gerada

O retorno final ao navegador pode ser a URL da imagem.

Exemplo

return response.getResult().getOutput().getUrl();

O que isso significa?

A API não está necessariamente devolvendo o arquivo binário da imagem diretamente para o navegador nesse exemplo. Ela está devolvendo uma resposta que contém a URL da imagem gerada.

Por que isso é útil?

Porque você pode:

  • abrir a URL em outra aba;
  • salvar a URL em banco;
  • armazenar a referência para uso posterior;
  • exibir a imagem em um front-end;
  • montar um fluxo mais completo de publicação ou biblioteca de mídias.

5.12 Exemplo completo do endpoint de imagem

import org.springframework.ai.image.ImageModel;
import org.springframework.ai.image.ImageOptionsBuilder;
import org.springframework.ai.image.ImagePrompt;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("imagem")
public class GeradorDeImagensController {

    private final ImageModel imageModel;

    public GeradorDeImagensController(ImageModel imageModel) {
        this.imageModel = imageModel;
    }

    @GetMapping
    public String gerarImagem(String prompt) {
        var options = ImageOptionsBuilder
                .builder()
                .withHeight(1024)
                .withWidth(1024)
                .build();

        var response = imageModel.call(new ImagePrompt(prompt, options));
        return response.getResult().getOutput().getUrl();
    }
}

Esse exemplo mostra uma implementação simples, clara e reaproveitável.


5.13 Testando a geração de imagem

Você pode testar chamando a rota no navegador com um parâmetro prompt.

Exemplo de chamada

GET /imagem?prompt=Paisagem ao amanhecer com montanhas suaves e névoa leve

O que deve acontecer?

  1. a aplicação recebe o prompt;
  2. chama a API de geração de imagem;
  3. a IA gera a imagem;
  4. a aplicação devolve uma URL;
  5. a URL pode ser aberta para visualizar o resultado.

O que você deve entender aqui?

Mesmo sendo um exemplo simples, esse fluxo já demonstra uma aplicação real de multimodalidade.


5.14 O código continua simples, mesmo com outro tipo de modelo

Esse é um ponto muito valioso do capítulo.

Embora agora estejamos lidando com imagem e não texto, o estilo de programação continua simples:

  • abstração do Spring;
  • criação de um prompt;
  • definição de opções;
  • chamada do modelo;
  • extração da resposta.

Isso reduz a curva de aprendizado, porque você percebe que o Spring AI mantém uma filosofia consistente entre diferentes tipos de recurso.


5.15 A grande vantagem: o código está desacoplado do provedor

Agora entramos em uma das ideias mais fortes do capítulo.

Classes como ImageModel, ChatClient e ChatOptionsBuilder pertencem ao pacote do Spring, não ao pacote de um provedor específico.

O que isso significa?

Significa que o código principal da aplicação está programado contra as abstrações do Spring AI, e não contra classes específicas da OpenAI.

Qual a vantagem disso?

Se amanhã você trocar de OpenAI para outro provedor suportado, o impacto no código tende a ser muito menor.


5.16 O que muda ao trocar de provedor?

Esse é o medo clássico de quem começa a trabalhar com APIs de IA:

“Se eu trocar de provedor, vou precisar reescrever tudo?”

Se a aplicação estiver muito acoplada ao SDK específico do fornecedor, talvez sim.
Mas se você estiver usando as abstrações do Spring AI, o impacto tende a se concentrar em poucos pontos.

Os principais pontos de mudança tendem a ser:

  • a dependência no pom.xml;
  • as propriedades no application.properties.

Essa é uma das maiores vantagens arquiteturais do Spring AI.


5.17 Impacto no pom.xml

Se o projeto usa uma dependência específica de provedor, como o starter da OpenAI, a troca de provedor passa por alterar ou complementar essa dependência.

Ideia geral

Antes:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>

Se você mudar para outro provedor compatível, trocará esse starter pelo correspondente.


5.18 Impacto no application.properties

O segundo ponto de impacto é a configuração.

Se você usa OpenAI, as propriedades têm prefixos como:

spring.ai.openai.api-key=...
spring.ai.openai.chat.options.model=...

Se trocar de provedor, o prefixo muda para o do novo fornecedor.

O que permanece semelhante?

Mesmo que os nomes exatos mudem, a lógica continua parecida:

  • há uma chave de API;
  • há propriedades de configuração;
  • há modelos;
  • há opções específicas;
  • há integração via abstrações do Spring.

5.19 O que não deveria precisar mudar?

Se sua aplicação estiver bem construída sobre as abstrações do Spring AI, o ideal é que o código principal das funcionalidades permaneça praticamente igual.

Isso vale especialmente para componentes que usam:

  • ChatClient
  • ImageModel
  • classes de prompt e response do Spring

Esse ponto é o coração arquitetural do capítulo.


5.20 O risco de usar classes específicas do provedor

É possível usar classes específicas de um provedor diretamente no código, mas isso aumenta o acoplamento.

O que é acoplamento, aqui?

É quando seu código depende demais da implementação específica de um fornecedor.

Qual o problema disso?

Se você mudar de provedor, talvez tenha que:

  • alterar imports;
  • reescrever chamadas;
  • mudar tipos de resposta;
  • adaptar opções específicas;
  • revisar várias classes do projeto.

Boa prática

Sempre que possível, use as classes do Spring AI:

  • facilita manutenção;
  • aumenta portabilidade;
  • reduz impacto de troca;
  • protege a arquitetura contra dependência excessiva de fornecedor.

5.21 O Spring AI como camada de abstração

Você pode pensar no Spring AI como uma camada intermediária entre:

  • sua aplicação;
  • e os fornecedores de IA.

Essa camada ajuda a evitar que o projeto fique “casado” com um único provedor.


5.22 Isso significa que trocar de provedor é sempre trivial?

Não necessariamente.

Embora o Spring AI ajude muito, ainda pode haver diferenças reais entre provedores, como:

  • disponibilidade de determinados recursos;
  • nomes e capacidades dos modelos;
  • parâmetros específicos;
  • formatos de resposta mais detalhados;
  • custo;
  • políticas de uso;
  • recursos multimodais suportados.

O que isso quer dizer?

A troca tende a ser muito mais simples, mas não significa que toda funcionalidade avançada será 100% idêntica entre todos os provedores.

Mesmo assim, o ganho arquitetural continua enorme.


5.23 O que você deve aprender com esse capítulo?

Mais do que decorar a API de imagem, ele deve entender três ideias grandes:

1. IA é multimodal

Não se resume a chat.

2. O Spring AI oferece abstrações específicas para cada tipo de modelo

Como ImageModel para imagem.

3. Programar contra abstrações do Spring AI reduz acoplamento

E facilita a troca de provedor no futuro.

Essas três ideias mudam bastante a sua maturidade como desenvolvedor.


5.24 Exemplo de uso real da geração de imagem

É importante que este ebook também mostre para que isso serviria na prática.

Possíveis aplicações

  • gerar capas ou ilustrações para artigos e documentação interna;
  • criar mockups rápidos;
  • produzir variações visuais para testes de interface;
  • gerar imagens para conteúdo educacional;
  • produzir material visual para marketing;
  • criar ilustrações para portais internos;
  • automatizar assets simples para testes.

O que observar em produção?

Mesmo que a geração de imagem funcione bem tecnicamente, ainda vale pensar em:

  • custo por geração;
  • tempo de resposta;
  • política de armazenamento;
  • expiração de URLs;
  • tamanho ideal da imagem;
  • política de moderação de conteúdo;
  • controle de prompts enviados pelos usuários.

Essas perguntas mostram que o recurso é poderoso, mas precisa ser usado com critério.


5.25 Boas práticas desta etapa

1. Use abstrações do Spring AI sempre que possível

Isso reduz acoplamento ao provedor.

2. Separe funcionalidades por controller ou serviço

Não misture tudo em um único lugar.

3. Defina opções de imagem de acordo com o caso de uso

Nem toda geração precisa ser 1024x1024.

4. Pense no destino da imagem

Vai apenas exibir? Salvar URL? Persistir metadados?

5. Avalie o impacto da troca de provedor desde cedo

Mesmo que você não vá trocar agora.

6. Não assuma que todos os provedores oferecem exatamente os mesmos recursos

Portabilidade ajuda, mas não elimina diferenças de plataforma.


5.26 Erros comuns nesta etapa

Erro 1: achar que Spring AI serve só para chat

Não serve.

Erro 2: usar diretamente classes específicas do fornecedor sem necessidade

Isso aumenta o acoplamento.

Erro 3: ignorar o formato e tamanho da imagem

Essas decisões afetam custo, uso e experiência.

Erro 4: devolver apenas a URL sem pensar no fluxo real da aplicação

Em projetos reais, talvez seja necessário salvar ou tratar mais dados.

Erro 5: acreditar que trocar de provedor nunca exige validação

Mesmo com abstração, diferenças práticas sempre merecem teste.


5.27 Resumo do capítulo

Neste capítulo, você aprendeu que o Spring AI não se limita à geração de texto.

Vimos que:

  • IA moderna é multimodal e pode trabalhar com texto, imagem e áudio;
  • a geração de imagem usa classes como ImageModel, ImagePrompt e ImageOptionsBuilder;
  • o resultado pode ser tratado como URL retornada pela API;
  • usar abstrações do Spring AI reduz o acoplamento ao provedor;
  • ao trocar de provedor, o impacto tende a ficar concentrado principalmente em dependências e propriedades;
  • a arquitetura do Spring AI ajuda a manter o código principal mais estável.

5.28 Exercícios propostos

1. Conceitual
Explique com suas palavras o que significa multimodalidade.

2. Conceitual
Qual a diferença entre usar ChatClient e usar ImageModel?

3. Prático
Crie um endpoint de geração de imagem que receba o prompt como parâmetro.

4. Arquitetura
Por que usar abstrações do Spring AI ajuda na troca de provedor?

5. Reflexão
Se uma aplicação troca OpenAI por outro provedor suportado, quais partes do projeto tendem a ser mais impactadas?


Desafio extra

Implemente um endpoint que gere duas imagens na mesma requisição (dois prompts diferentes) e devolva um JSON com duas URLs. Trate falha de uma das gerações sem derrubar a outra (try/catch por chamada) e documente o comportamento.


5.29 Fechamento

Com este capítulo, você fecha uma etapa importante do curso entendendo que:

  • o Spring AI vai além do chat;
  • a geração de imagem segue a mesma filosofia de simplicidade;
  • a arquitetura do framework foi pensada para reduzir dependência de fornecedor.

Esse é um excelente fechamento conceitual, porque prepara você para pensar em IA de forma mais ampla, mais arquitetural e mais profissional.


Capítulo 6 — Tool Calling no Spring AI: fazendo a IA usar ferramentas da aplicação

Objetivo do capítulo

Neste capítulo, você vai aprender:

  • o que é tool calling;
  • por que esse recurso muda o nível da aplicação;
  • a diferença entre o modelo “responder com base no que sabe” e “usar uma ferramenta”;
  • como transformar métodos Java em tools com Spring AI;
  • como expor tools ao modelo usando ChatClient;
  • como usar tool calling em cenários de consulta e ação;
  • quais cuidados tomar ao deixar a IA acionar funcionalidades da aplicação.

Ao final, você deve entender a seguinte ideia:

uma aplicação com IA fica muito mais útil quando o modelo não apenas conversa, mas também consegue pedir acesso a funções reais do sistema.


6.1 Introdução

Até aqui, trabalhamos principalmente com prompts, parâmetros, modelos, erros e multimodalidade. Em todos esses casos, a aplicação enviava uma solicitação ao modelo e recebia uma resposta. (O MCP será tema do Capítulo 7; aqui o foco é tool calling com ferramentas definidas na sua aplicação.)

Mas existe um limite natural nesse fluxo.

O modelo pode gerar texto muito bem, mas ele não conhece automaticamente o estado atual do seu sistema, nem pode executar ações no mundo real por conta própria. Por exemplo, ele não sabe sozinho:

  • a data e hora exatas do servidor;
  • se existe um registro cadastrado no seu banco;
  • qual é o status atual de um chamado;
  • se um alarme foi configurado;
  • se um e-mail foi realmente enviado;
  • se um documento foi emitido no sistema.

É aqui que entra o tool calling.


6.2 O que é tool calling?

Tool calling é a capacidade de permitir que o modelo solicite o uso de uma ferramenta externa durante uma conversa.

Em linguagem simples

Pense assim:

  • sem tool calling, o modelo só responde com base no que recebeu no prompt e no que “sabe” pelo treinamento;
  • com tool calling, o modelo pode dizer algo como:
    • “Para responder corretamente, preciso consultar esta ferramenta.”

A partir daí:

  1. o modelo escolhe uma tool;
  2. informa o nome da tool;
  3. envia os parâmetros necessários;
  4. a aplicação executa a tool;
  5. o resultado volta ao modelo;
  6. o modelo gera a resposta final usando esse resultado como contexto.
sequenceDiagram autonumber actor U as Usuário participant B as Backend(Spring AI) participant M as Modelo de IA participant T as Tool(método Java) U->>B: Envia pergunta (ex: "Qual o status do chamado CH-123?") B->>M: Envia prompt com tools disponíveis M->>B: Solicita execução da tool (nome + parâmetros) B->>T: Executa a tool com os parâmetros T-->>B: Retorna resultado (ex: "Chamado CH-123: EM ANÁLISE") B->>M: Envia resultado da tool como contexto M-->>B: Gera resposta final com base no resultado B-->>U: Entrega resposta ao usuário

6.3 Por que isso é tão importante?

Porque esse recurso muda completamente o tipo de sistema que podemos construir.

Sem tool calling, a IA fica mais limitada a:

  • explicar;
  • sugerir;
  • resumir;
  • classificar;
  • gerar texto.

Com tool calling, ela pode participar de fluxos mais úteis, como:

  • consultar dados atuais;
  • buscar informações do sistema;
  • acionar processos;
  • combinar raciocínio com dados reais;
  • orquestrar pequenas automações.

6.4 A IA executa a ferramenta sozinha?

Não.

Esse ponto é essencial e precisa ficar muito claro para você.

O modelo não executa código Java diretamente.
Ele não chama o banco sozinho.
Ele não dispara métodos da aplicação por mágica.

Quem executa a tool é a aplicação.

O papel do modelo

O modelo:

  • recebe a lista de tools disponíveis;
  • analisa a pergunta;
  • decide se precisa usar uma tool;
  • informa qual tool quer usar e com quais parâmetros.

O papel da aplicação

A aplicação:

  • identifica a tool solicitada;
  • executa a tool com os parâmetros recebidos;
  • coleta o resultado;
  • envia esse resultado de volta ao modelo.

6.5 Quando usar tool calling?

Tool calling faz sentido quando a resposta depende de algo que está fora do conhecimento puro do modelo.

Exemplos de uso

Buscar informação

  • hora atual;
  • clima;
  • status de chamado ou tarefa;
  • dados cadastrais ou de perfil;
  • resultado de consulta;
  • prazo ou SLA retornado pelo sistema;
  • agenda disponível.

Executar ação

  • criar lembrete;
  • disparar rotina;
  • gerar documento;
  • registrar evento;
  • atualizar status;
  • programar alarme;
  • acionar um serviço interno.

6.6 O que são ToolCallback e ToolCallingManager?

Na prática, o Spring AI já resolve boa parte do trabalho operacional, então o desenvolvedor pode focar em:

  • definir bem a tool;
  • descrever bem o que ela faz;
  • expor essa tool ao modelo;
  • validar se o comportamento ficou coerente.

Em linguagem simples

ToolCallback
Representa a ferramenta que pode ser disponibilizada ao modelo.

ToolCallingManager
Cuida do processo de execução das tools.


6.7 Formas de definir tools no Spring AI

Existem duas formas principais de transformar métodos em tools:

  • de forma declarativa, com @Tool;
  • de forma programática, com implementações mais manuais.

Para um ebook introdutório e didático, a melhor porta de entrada é a abordagem com @Tool.


6.8 Criando uma tool com @Tool

A forma mais simples de começar é transformar um método Java em tool anotando-o com @Tool.

Exemplo simples: data e hora atual

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component;

import java.time.ZonedDateTime;

@Component
public class DataHoraTools {

    @Tool(description = "Obtém a data e hora atuais do sistema")
    public String obterDataHoraAtual() {
        return ZonedDateTime.now().toString();
    }
}

O que esse código faz?

  • cria uma classe Spring;
  • define um método Java comum;
  • marca esse método como tool;
  • descreve claramente o propósito da tool.

Por que a descrição importa tanto?

Se a descrição for ruim, o modelo pode:

  • não usar a tool quando deveria;
  • usar a tool de forma errada;
  • confundir a finalidade da ferramenta.

6.9 Entendendo os elementos da anotação @Tool

Ao usar @Tool, alguns pontos merecem destaque:

name

É o nome da tool. Se você não informar, o nome do método será usado.

description

É a descrição da ferramenta. É um dos pontos mais importantes, porque ajuda o modelo a decidir quando usar a tool.

returnDirect

Define se o resultado deve voltar direto ao cliente ou se deve primeiro voltar ao modelo.

Para o começo deste ebook, o essencial é você dominar bem description e entender a ideia de returnDirect.


6.10 Como disponibilizar a tool ao modelo

Depois de criar a tool, precisamos torná-la disponível para a conversa.

Exemplo de uso com ChatClient

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.stereotype.Service;

@Service
public class AssistenteService {

    private final ChatClient chatClient;
    private final DataHoraTools dataHoraTools;

    public AssistenteService(ChatModel chatModel, DataHoraTools dataHoraTools) {
        this.chatClient = ChatClient.create(chatModel);
        this.dataHoraTools = dataHoraTools;
    }

    public String responder(String pergunta) {
        return chatClient.prompt()
                .user(pergunta)
                .tools(dataHoraTools)
                .call()
                .content();
    }
}

O que está acontecendo aqui?

  1. criamos o ChatClient;
  2. registramos a tool com .tools(dataHoraTools);
  3. enviamos a pergunta do usuário;
  4. se o modelo precisar da tool, o Spring AI cuida do fluxo.

6.11 Exemplo prático de uso

Imagine que o usuário pergunte:

Que horas são agora?

Se a tool estiver bem descrita e disponível, o modelo pode decidir usar a ferramenta obterDataHoraAtual().

Fluxo esperado

  1. usuário pergunta;
  2. modelo percebe que precisa de dado atual;
  3. modelo pede a tool;
  4. Spring executa a tool;
  5. o resultado volta ao modelo;
  6. o modelo responde algo como:
    • “Agora são 14:23 no horário do sistema.”

6.12 Primeiro caso de uso: tools para buscar informação

Esse é o cenário mais simples e mais seguro para começar.

Exemplo: buscar status de chamado

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component;

@Component
public class ChamadoTools {

    @Tool(description = "Consulta o status atual de um chamado de suporte a partir do protocolo informado")
    public String consultarStatusChamado(String protocolo) {
        if ("CH-123".equals(protocolo)) {
            return "Chamado CH-123 está com status: EM ANÁLISE";
        }
        return "Chamado não encontrado";
    }
}

Serviço com ChatClient

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.stereotype.Service;

@Service
public class ChamadoAssistenteService {

    private final ChatClient chatClient;
    private final ChamadoTools chamadoTools;

    public ChamadoAssistenteService(ChatModel chatModel, ChamadoTools chamadoTools) {
        this.chatClient = ChatClient.create(chatModel);
        this.chamadoTools = chamadoTools;
    }

    public String responder(String pergunta) {
        return chatClient.prompt()
                .system("Você é um assistente de suporte e deve usar tools quando precisar consultar dados reais.")
                .user(pergunta)
                .tools(chamadoTools)
                .call()
                .content();
    }
}

Exemplo de pergunta

Qual o status do chamado CH-123?

O que esperamos?

O modelo deve entender que não sabe o status por conta própria e usar a tool consultarStatusChamado.


6.13 Segundo caso de uso: tools para executar ação

Agora vamos para um caso mais forte.

Exemplo: criar lembrete

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component;

@Component
public class LembreteTools {

    @Tool(description = "Cria um lembrete com a mensagem e o horário informados")
    public String criarLembrete(String mensagem, String horario) {
        return "Lembrete criado com sucesso para " + horario + ": " + mensagem;
    }
}

Serviço com duas tools

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.stereotype.Service;

@Service
public class ProdutividadeAssistenteService {

    private final ChatClient chatClient;
    private final DataHoraTools dataHoraTools;
    private final LembreteTools lembreteTools;

    public ProdutividadeAssistenteService(
            ChatModel chatModel,
            DataHoraTools dataHoraTools,
            LembreteTools lembreteTools
    ) {
        this.chatClient = ChatClient.create(chatModel);
        this.dataHoraTools = dataHoraTools;
        this.lembreteTools = lembreteTools;
    }

    public String responder(String pergunta) {
        return chatClient.prompt()
                .user(pergunta)
                .tools(dataHoraTools, lembreteTools)
                .call()
                .content();
    }
}

Exemplo de pergunta

Crie um lembrete para daqui a 10 minutos me lembrar de beber água.

Fluxo provável

  1. o modelo percebe que precisa saber o horário atual;
  2. chama a tool de data/hora;
  3. calcula o horário desejado;
  4. chama a tool de criação de lembrete;
  5. devolve a resposta final ao usuário.

6.14 O que significa returnDirect?

Quando returnDirect=true, o resultado da tool pode ser devolvido diretamente ao cliente, sem que o modelo precise transformar esse resultado em resposta final.

Quando isso pode ser útil?

Quando você quer devolver o resultado bruto da ferramenta.

Exemplo

@Tool(description = "Busca informações resumidas de um colaborador interno pelo id", returnDirect = true)
public String buscarColaborador(Long id) {
    return "Colaborador 10: Henrique Santiago";
}

O que muda?

Em vez de o modelo usar o resultado como contexto para montar uma frase, a aplicação pode devolver esse resultado diretamente.

Quando eu evitaria no começo?

Para capítulos iniciais, eu deixaria returnDirect=false na maioria dos casos, porque você primeiro precisa entender o fluxo padrão:

  • modelo pede a tool;
  • tool executa;
  • modelo responde.

6.15 Tipos de parâmetros e retorno nas tools

As tools não precisam ficar restritas apenas a uma string simples.

Exemplo com objeto

public record ConsultaChamadoInput(String protocolo, String area) {}
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component;

@Component
public class SuporteTools {

    @Tool(description = "Consulta um chamado de suporte pelo protocolo e pela área informados")
    public String consultarChamado(ConsultaChamadoInput input) {
        return "Chamado consultado: " + input.protocolo() + " / " + input.area();
    }
}

Isso abre caminho para tools mais organizadas e mais próximas do domínio real da aplicação.


6.16 Onde colocar as tools no projeto?

Em projeto pequeno

Você pode colocar tools em classes simples como:

  • DataHoraTools
  • ChamadoTools
  • LembreteTools

Em projeto maior

O ideal é separar por responsabilidade:

  • CadastroTools
  • ChamadoTools
  • FinanceiroTools
  • AgendaTools

Boa prática

A tool deve ser uma porta de acesso clara para uma capacidade do sistema.
Ela não deve virar um “super método genérico” que faz tudo.


6.17 Onde o modelo decide usar a tool?

O modelo decide usar a tool com base em três coisas:

  • a pergunta do usuário;
  • a descrição da tool;
  • o contexto do prompt.

Por isso, muitas vezes não basta só registrar a ferramenta. Vale também orientar no system quando o modelo deve usá-la.

Exemplo de system útil

.system("Você é um assistente do sistema. Sempre que a pergunta depender de dados atuais ou ações do sistema, utilize as tools disponíveis.")

Isso ajuda bastante o comportamento.


6.18 O que pode dar errado com tools?

Muita coisa pode dar errado se a tool for mal projetada.

Problemas comuns

1. Descrição ruim
O modelo não entende quando usar a tool.

2. Tool ambígua
Duas ferramentas parecem fazer a mesma coisa.

3. Nome pouco claro
Aumenta a chance de uso incorreto.

4. Ação perigosa demais
O modelo pode pedir algo que deveria passar por validação humana.

5. Retorno confuso
Se a tool devolve algo bagunçado, a resposta final também pode ficar ruim.


6.19 Cuidados de segurança com tool calling

Tool calling é poderoso, mas também pode ser perigoso se mal usado.

Nunca exponha sem critério tools que:

  • apagam dados;
  • alteram registros sensíveis;
  • disparam pagamentos;
  • aprovam processos críticos;
  • executam comandos administrativos;
  • acessam dados pessoais sem controle.

Melhor abordagem

Comece com tools de leitura e baixo risco, como:

  • hora atual;
  • status de chamado;
  • consulta cadastral de baixo risco;
  • agenda disponível;
  • artigo na base de conhecimento interna.

Depois, avance para ações com mais responsabilidade e, se necessário, inclua camadas extras de validação.


6.20 Tool calling não substitui regra de negócio

A IA não deve assumir o papel da regra de negócio central.

A tool chama sua aplicação.
Mas a sua aplicação continua responsável por:

  • validações;
  • regras;
  • permissões;
  • restrições;
  • auditoria;
  • consistência.

Ou seja: a IA orquestra a necessidade. O sistema continua sendo o dono da regra.


6.21 Exemplo de controller para uso prático

Para fechar a parte de implementação, aqui vai um exemplo simples de controller usando tool calling.

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/assistente")
public class AssistenteController {

    private final ChamadoAssistenteService chamadoAssistenteService;

    public AssistenteController(ChamadoAssistenteService chamadoAssistenteService) {
        this.chamadoAssistenteService = chamadoAssistenteService;
    }

    @GetMapping
    public String responder(@RequestParam String pergunta) {
        return chamadoAssistenteService.responder(pergunta);
    }
}

Exemplo de chamada

GET /assistente?pergunta=Qual o status do chamado CH-123?

Resultado esperado

Se o fluxo estiver bem configurado, o modelo vai usar a tool de consulta de chamado e responder com base no resultado real da aplicação.


6.22 O que você precisa guardar deste capítulo?

Mais do que decorar a anotação @Tool, ele precisa entender estas ideias:

  1. o modelo não sabe tudo e não executa ações sozinho;
  2. tools conectam o modelo às capacidades reais da aplicação;
  3. o Spring AI já oferece o mecanismo para isso;
  4. descrição da tool é parte crítica da implementação;
  5. tool calling serve tanto para consulta quanto para ação;
  6. o sistema continua responsável por segurança e regra de negócio.

6.23 Boas práticas desta etapa

1. Dê descrições excelentes para as tools

A descrição é fundamental para o modelo usar a tool corretamente.

2. Comece com tools simples

Exemplo: data/hora, consulta de status de chamado, consulta cadastral simples.

3. Use nomes claros

Ferramenta precisa ser facilmente identificável.

4. Separe tools por domínio

Não concentre tudo em uma única classe gigante.

5. Use system para orientar o modelo

Ajuda bastante na decisão de uso.

6. Tenha cuidado com tools de ação

Nem tudo deve ser acionado automaticamente.

7. Faça logs e observabilidade

Especialmente em tools que tocam sistema real.


6.24 Erros comuns nesta etapa

Erro 1: achar que a IA vai usar a tool automaticamente sem boa descrição

Não necessariamente vai.

Erro 2: criar tool genérica demais

Isso confunde o modelo.

Erro 3: expor ação sensível cedo demais

Isso aumenta risco.

Erro 4: esquecer que a aplicação executa a tool

Tool calling não elimina responsabilidade do backend.

Erro 5: misturar tool com regra de negócio pesada no controller

A tool deve chamar lógica organizada, não virar um atalho bagunçado.


6.25 Resumo do capítulo

Neste capítulo, você aprendeu que o Spring AI permite expor ferramentas da aplicação ao modelo por meio de tool calling. Vimos que:

  • tools conectam o modelo às capacidades reais da aplicação;
  • o modelo não executa a ferramenta sozinho, a aplicação executa;
  • @Tool permite transformar métodos Java em tools de forma simples;
  • a descrição da tool é essencial para o modelo entender quando usá-la;
  • ChatClient pode receber tools diretamente com .tools(...);
  • tool calling é útil tanto para busca de informação quanto para execução de ação;
  • a aplicação continua responsável por segurança, validação e regra de negócio.

6.26 Exercícios propostos

1. Conceitual
Explique com suas palavras a diferença entre uma resposta normal do modelo e uma resposta baseada em tool calling.

2. Conceitual
Por que a descrição de uma tool é tão importante?

3. Prático
Crie uma tool chamada consultarResumoCadastral e descreva o que ela deveria fazer (sem expor dados sensíveis).

4. Prático
Monte um ChatClient que disponibilize duas tools para o modelo:

  • uma para obter data/hora;
  • outra para consultar status de chamado.

5. Reflexão
Por que tools de ação exigem mais cuidado do que tools de consulta?


Desafio extra

Implemente uma tool buscarCep(String cep) que valide o formato (apenas dígitos / tamanho) antes de chamar qualquer API externa e retorne erro claro ao modelo se for inválido. Escreva 3 perguntas em linguagem natural e mostre quando o modelo escolhe a tool ou responde só com texto.


6.27 Fechamento

Com este capítulo, você sai do uso “reativo” da IA e entra em um cenário mais poderoso: a IA agora pode interagir com capacidades reais da aplicação.

Esse é um passo muito importante porque abre caminho para:

  • assistentes conectados ao sistema;
  • automações orientadas por linguagem natural;
  • fluxos mais inteligentes;
  • aplicações mais úteis no mundo real.

Capítulo 7 — MCP no Spring AI: conectando modelos, ferramentas e recursos externos por um protocolo padronizado

Objetivo do capítulo

Neste capítulo, você vai aprender:

  • o que é MCP;
  • por que ele está ganhando tanta importância em aplicações com IA;
  • como o MCP se relaciona com tools, resources e prompts;
  • a diferença entre cliente MCP e servidor MCP;
  • quais transportes o protocolo suporta;
  • como consumir um servidor MCP em uma aplicação Spring AI;
  • como expor um servidor MCP com Spring Boot;
  • quando usar MCP em vez de integrações manuais;
  • como isso se conecta com tool calling.

Ao final, você deve entender a seguinte ideia:

o MCP é uma forma padronizada de ligar modelos de IA a capacidades externas sem criar uma integração improvisada para cada caso.


7.1 Introdução

No capítulo anterior, vimos tool calling: o modelo pode pedir o uso de uma ferramenta, e a aplicação executa essa ferramenta.

Aquilo já representa um grande salto. Mas surge uma nova pergunta:

“E se eu quiser que minha aplicação converse com ferramentas externas de forma mais padronizada?”

Por exemplo:

  • um servidor que expõe arquivos;
  • um serviço que oferece tools de clima;
  • um sistema que expõe recursos internos;
  • um catálogo de prompts reutilizáveis;
  • uma fonte remota de capabilities que podem ser descobertas em tempo de execução.

Se cada integração for feita manualmente, o projeto pode ficar:

  • muito acoplado;
  • difícil de manter;
  • inconsistente entre fornecedores;
  • complicado de evoluir.

É exatamente esse tipo de problema que o MCP busca resolver.


7.2 O que é MCP?

MCP significa Model Context Protocol.

Em linguagem simples

É um protocolo que define uma forma padronizada de:

  • descobrir ferramentas;
  • acessar recursos;
  • usar prompts;
  • trocar mensagens entre cliente e servidor;
  • permitir que aplicações com IA se conectem a capacidades externas sem inventar uma solução diferente para cada integração.

Uma analogia útil

Pense no MCP como uma espécie de “tomada padrão” para IA.

Sem MCP, cada ferramenta precisaria de um encaixe diferente.
Com MCP, você passa a ter uma interface comum para conectar capacidades externas.


7.3 Por que o MCP é importante?

O MCP é importante porque ajuda a resolver um problema clássico de arquitetura:

como conectar IA a sistemas externos de forma organizada e reaproveitável?

Sem um protocolo padronizado, cada integração pode exigir:

  • contrato próprio;
  • modo próprio de descoberta;
  • serialização própria;
  • autenticação própria;
  • convenções próprias;
  • adaptação manual em cada projeto.

Com MCP, a ideia é ter uma interface comum para expor capacidades como:

  • Tools
  • Resources
  • Prompts

7.4 Qual a relação entre MCP e Tool Calling?

Esse ponto é muito importante.

No capítulo anterior, vimos o modelo usando tools definidas localmente pela aplicação.
Com MCP, essas tools podem estar fora da aplicação, em um servidor MCP.

Em outras palavras

  • Tool calling é a capacidade do modelo de pedir uma ferramenta;
  • MCP é uma forma padronizada de disponibilizar essas ferramentas, recursos e prompts.

Então podemos pensar assim:

tool calling responde à pergunta “como o modelo usa uma ferramenta?”
MCP responde à pergunta “como essa ferramenta é exposta e descoberta de forma padronizada?”


7.5 Cliente MCP e servidor MCP

O MCP tem dois papéis principais.

Cliente MCP

É quem se conecta a um servidor MCP para consumir as capacidades expostas por ele.

Servidor MCP

É quem expõe capacidades para clientes.

Analogia prática

  • o cliente MCP é quem quer usar recursos;
  • o servidor MCP é quem disponibiliza esses recursos.

7.6 A arquitetura em camadas do MCP

O MCP pode ser entendido em três camadas:

  • Camada de Cliente/Servidor
  • Camada de Sessão
  • Camada de Transporte

O que isso significa de forma didática?

Camada de Cliente/Servidor

É a camada onde moram os conceitos principais:

  • McpClient
  • McpServer

Camada de Sessão

Gerencia o estado da comunicação.

Camada de Transporte

Cuida da troca real de mensagens.

O que você precisa guardar?

Mesmo que ele não programe diretamente nessas camadas no começo, é importante entender que o MCP não é só uma anotação. Existe uma estrutura protocolar por trás.


7.7 Quais capacidades um servidor MCP pode expor?

Um servidor MCP pode expor várias capabilities.

As mais importantes para começar

  • Tools — ferramentas invocáveis por modelos;
  • Resources — acesso padronizado a conteúdos e dados;
  • Prompts — templates de prompt expostos aos clientes.

Esses três já explicam muito bem o valor do MCP.


7.8 O que são resources no MCP?

Como esse termo aparece bastante, vale explicar.

Resource

É uma forma padronizada de expor um conteúdo acessável por URI.

Exemplo conceitual:

  • config://tema
  • arquivo://manual-interno
  • cliente://123
  • faq://matricula

O servidor MCP pode disponibilizar esse recurso, e o cliente pode acessá-lo de forma estruturada.


7.9 O que são prompts no MCP?

Além de tools e resources, o MCP também pode expor prompts.

Em linguagem simples

Um servidor MCP pode disponibilizar prompts prontos ou templates que outros clientes podem usar.

Isso é útil quando se quer centralizar instruções, fluxos ou comportamentos reutilizáveis.


7.10 Quais transportes o MCP suporta?

O MCP suporta múltiplos mecanismos de transporte. Entre os mais conhecidos estão:

  • STDIO
  • SSE
  • Streamable-HTTP

Explicando cada um

STDIO

Comunicação por entrada e saída padrão. É muito usada quando o servidor roda como processo local.

SSE

Server-Sent Events, útil para atualizações em tempo real.

Streamable-HTTP

Modelo mais moderno de comunicação HTTP com possibilidade de mensagens múltiplas.

O que você precisa entender?

Não precisa dominar todos os transportes agora.
Mas precisa saber que o MCP não depende de um único meio de conexão.


7.11 Quando usar MCP em vez de integração manual?

Essa é uma pergunta central.

MCP faz mais sentido quando:

  • você quer consumir ferramentas externas já expostas em protocolo padrão;
  • deseja reutilizar capabilities em múltiplos clientes;
  • quer evitar reinventar contrato para cada integração;
  • precisa descobrir tools dinamicamente;
  • quer separar melhor cliente e fornecedor de capabilities.

Integração manual ainda pode fazer sentido quando:

  • o caso é simples;
  • você controla totalmente os dois lados;
  • não há necessidade de padronização com outros clientes;
  • a solução seria menor que o custo de introduzir MCP.

Regra prática

Se a integração é pequena e totalmente local, @Tool pode bastar.
Se você quer padronização, descoberta e interoperabilidade, MCP começa a fazer muito sentido.


7.12 Spring AI e MCP no lado cliente

Quando sua aplicação é cliente MCP, ela consome capacidades expostas por um servidor MCP.

O que isso traz de vantagem?

Você consegue conectar sua aplicação Spring a um servidor MCP usando auto-configuração, em vez de montar tudo manualmente.

Tipos de uso

  • consumir tools de um servidor externo;
  • acessar resources remotos;
  • reutilizar prompts disponibilizados por outra aplicação.

7.13 Propriedades do cliente MCP

Ao configurar um cliente MCP, algumas propriedades fazem bastante sentido para você conhecer.

As mais importantes para começar

spring.ai.mcp.client.type
Define se o cliente será síncrono ou assíncrono.

spring.ai.mcp.client.request-timeout
Define timeout das requisições MCP.

spring.ai.mcp.client.toolcallback.enabled
Ativa a integração das tools MCP com o mecanismo de tool calling do Spring AI.

O que isso significa?

Significa que o cliente MCP pode ser ajustado conforme o comportamento da aplicação e pode inclusive converter tools remotas em ferramentas utilizáveis pelo ChatClient.


7.14 Exemplo 1 — Consumindo um servidor MCP por STDIO

Uma forma de consumir um servidor MCP é configurando uma conexão por STDIO.

Exemplo de configuração

spring:
  ai:
    mcp:
      client:
        stdio:
          connections:
            filesystem-server:
              command: /path/to/server
              args:
                - --mode=production
              env:
                API_KEY: minha-chave
                DEBUG: "true"

O que isso significa?

Sua aplicação Spring vira cliente MCP e sobe um processo externo que funciona como servidor MCP, comunicando-se por STDIO.

Quando isso é útil?

  • ferramenta local;
  • servidor rodando na mesma máquina;
  • integração com utilitários de processo;
  • cenários de desktop ou automação local.

7.15 Exemplo 2 — Consumindo um servidor MCP por Streamable-HTTP

Outra opção é conectar-se a um servidor MCP remoto via HTTP.

Exemplo

spring:
  ai:
    mcp:
      client:
        streamable-http:
          connections:
            server1:
              url: http://localhost:8080
            server2:
              url: http://otherserver:8081
              endpoint: /custom-sse

O que isso significa?

Sua aplicação cliente pode conectar-se a servidores MCP remotos via HTTP.

Quando isso é útil?

  • arquitetura distribuída;
  • microserviços;
  • servidores MCP independentes;
  • ambientes cloud.

7.16 As tools MCP podem virar ToolCallbacks no Spring AI

Esse é um ponto muito importante para conectar MCP com o capítulo anterior.

Quando o cliente MCP está configurado para isso, as tools disponíveis em um servidor MCP podem ser transformadas em ferramentas utilizáveis pela aplicação Spring AI.

O que isso significa na prática?

Significa que um servidor MCP pode expor tools, e sua aplicação cliente Spring AI pode reaproveitar essas tools dentro do fluxo de tool calling.

Esse é um dos pontos mais fortes do MCP no ecossistema Spring AI.


7.17 Exemplo de uso das tools MCP no cliente

Um uso comum é recuperar as tools expostas pelo servidor MCP e passá-las ao ChatClient.

Exemplo didático

import org.springframework.ai.tool.ToolCallback;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class McpToolService {

    @Autowired
    private SyncMcpToolCallbackProvider toolCallbackProvider;

    public ToolCallback[] obterToolsDisponiveis() {
        return toolCallbackProvider.getToolCallbacks();
    }
}

O que isso faz?

Recupera as tools expostas pelos servidores MCP conectados.

O próximo passo natural

Essas tools podem então ser passadas ao ChatClient, do mesmo jeito que fizemos com tools locais no capítulo anterior.


7.18 Exemplo prático: usando tools MCP com ChatClient

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.stereotype.Service;

@Service
public class AssistenteMcpService {

    private final ChatClient chatClient;
    private final SyncMcpToolCallbackProvider toolCallbackProvider;

    public AssistenteMcpService(ChatModel chatModel, SyncMcpToolCallbackProvider toolCallbackProvider) {
        this.chatClient = ChatClient.create(chatModel);
        this.toolCallbackProvider = toolCallbackProvider;
    }

    public String responder(String pergunta) {
        ToolCallback[] tools = toolCallbackProvider.getToolCallbacks();

        return chatClient.prompt()
                .system("Você é um assistente que pode usar tools MCP quando precisar de dados externos.")
                .user(pergunta)
                .tools(tools)
                .call()
                .content();
    }
}

O que esse exemplo mostra?

  • a aplicação consome um servidor MCP;
  • as tools desse servidor viram ToolCallbacks;
  • o ChatClient passa a poder usá-las em conversa com o modelo.

Esse é um ótimo ponto de conexão entre tool calling local e capabilities remotas via MCP.


7.19 Spring AI e MCP no lado servidor

Agora vamos inverter a visão.

Em vez de consumir um servidor MCP, podemos criar um servidor MCP com Spring Boot.

O que isso permite?

Transformar sua aplicação Spring em um servidor MCP que expõe:

  • tools;
  • resources;
  • prompts;
  • e outras capacidades úteis para clientes MCP.

7.20 Configurando um servidor MCP

A configuração do servidor depende da forma como você quer expor o protocolo.

Exemplo para STDIO

spring.ai.mcp.server.stdio=true

Exemplo para Streamable-HTTP

spring.ai.mcp.server.protocol=STREAMABLE

O que você deve entender?

O protocolo e a forma de exposição precisam estar coerentes com o tipo de integração que você deseja.


7.21 Anotações do lado servidor MCP

No lado servidor, o Spring AI permite desenvolvimento declarativo com anotações específicas.

Anotações principais

  • @McpTool
  • @McpResource
  • @McpPrompt

Essas anotações ajudam a expor capacidades do servidor de forma clara e organizada.


7.22 Exemplo 3 — Criando um servidor MCP com @McpTool

Exemplo

import org.springframework.ai.mcp.annotation.McpTool;
import org.springframework.ai.mcp.annotation.McpToolParam;
import org.springframework.stereotype.Component;

@Component
public class CalculatorTools {

    @McpTool(name = "add", description = "Somar dois números")
    public int add(
            @McpToolParam(description = "Primeiro número", required = true) int a,
            @McpToolParam(description = "Segundo número", required = true) int b) {
        return a + b;
    }
}

O que isso faz?

Sua aplicação Spring passa a expor uma tool MCP chamada add, acessível a clientes MCP.


7.23 Exemplo 4 — Expondo um resource com @McpResource

Exemplo didático

import org.springframework.ai.mcp.annotation.McpResource;
import org.springframework.stereotype.Component;

import java.util.Map;

@Component
public class ConfigResources {

    private final Map<String, String> configData = Map.of(
            "tema", "escuro",
            "idioma", "pt-BR"
    );

    @McpResource(uri = "config://{key}", name = "Configuration")
    public String getConfig(String key) {
        return configData.getOrDefault(key, "não encontrado");
    }
}

O que isso ensina?

O servidor MCP pode expor recursos acessáveis por URI, e não apenas tools executáveis.


7.24 Exemplo 5 — Expondo prompts com @McpPrompt

Exemplo didático

import org.springframework.ai.mcp.annotation.McpPrompt;
import org.springframework.stereotype.Component;

@Component
public class PromptTemplates {

    @McpPrompt(name = "triar-chamado", description = "Prompt para triagem de chamados de suporte")
    public String triarChamadoPrompt() {
        return """
                Você classifica chamados de suporte em uma única etiqueta de fila.
                Responda apenas com uma das categorias válidas:
                Acesso e autenticação, Bug ou estabilidade, Faturamento e cobrança, Outros.
                """;
    }
}

O que isso mostra?

O servidor MCP também pode centralizar prompts reutilizáveis para outros clientes.


7.25 Auto-configuração do servidor MCP

Em uma aplicação Spring Boot, a auto-configuração pode detectar automaticamente os beans anotados e registrá-los no servidor MCP.

Exemplo de aplicação principal

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class McpServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(McpServerApplication.class, args);
    }
}

Configuração mínima

spring:
  ai:
    mcp:
      server:
        type: SYNC
        annotation-scanner:
          enabled: true

7.26 Cliente MCP ou servidor MCP: quando usar cada um?

Use cliente MCP quando:

  • sua aplicação quer consumir capabilities expostas por outro sistema;
  • você quer usar tools/resources remotos;
  • deseja integrar-se a servidores MCP prontos.

Use servidor MCP quando:

  • sua aplicação quer expor suas próprias capabilities;
  • você quer disponibilizar tools, resources ou prompts para outros clientes;
  • deseja transformar um serviço Spring em fonte padronizada de contexto e ação.

Em alguns cenários, você pode ter os dois

Uma aplicação pode:

  • consumir um servidor MCP externo;
  • e também expor suas próprias capabilities como servidor MCP.

7.27 Sync e Async no MCP

Clientes e servidores MCP podem operar em modo síncrono ou assíncrono.

O que isso significa?

Você precisa manter coerência com o modelo de programação da sua aplicação.

  • se a aplicação é mais simples e direta, o modo síncrono costuma bastar;
  • se há fluxos reativos ou necessidade de maior escala, o modo assíncrono pode fazer mais sentido.

7.28 Benefícios arquiteturais do MCP

Principais benefícios

1. Padronização
Você deixa de criar uma integração artesanal para cada capability.

2. Descoberta de ferramentas
Clientes podem descobrir tools expostas por servidores.

3. Reuso
Uma capability pode atender múltiplos clientes.

4. Desacoplamento
O cliente não precisa conhecer detalhes internos do servidor.

5. Evolução mais saudável
O protocolo ajuda a manter contratos mais consistentes.

6. Integração com Spring AI
Tools MCP podem alimentar o fluxo de tool calling do ChatClient.


7.29 Quando o MCP pode ser “demais”?

Nem todo projeto precisa começar com MCP.

Pode ser excesso quando:

  • o sistema é pequeno;
  • há uma única integração simples;
  • você controla totalmente os dois lados;
  • @Tool local já resolve com clareza.

Faz mais sentido quando:

  • há múltiplas capabilities;
  • você quer interoperabilidade;
  • a solução precisa crescer;
  • vários clientes consumirão as mesmas ferramentas;
  • o padrão de exposição traz benefício real.

7.30 Cuidados práticos ao usar MCP

1. Não comece pelo transporte, comece pelo problema

Escolha STDIO, SSE ou HTTP só depois de entender o caso de uso.

2. Mantenha tools e resources claros

Protocolo padronizado não salva design ruim.

3. Documente bem as capacidades expostas

Nome ruim e descrição ruim continuam sendo problema.

4. Pense em segurança

Expor capabilities externas exige responsabilidade.

5. Use observabilidade

Especialmente quando há múltiplas conexões e ferramentas externas.


7.31 Erros comuns nesta etapa

Erro 1: achar que MCP substitui todo tipo de integração

Ele ajuda muito, mas não é solução universal.

Erro 2: usar MCP sem necessidade real

Às vezes uma tool local basta.

Erro 3: confundir cliente e servidor

Consumir capabilities e expor capabilities são papéis diferentes.

Erro 4: não pensar no transporte

O meio de comunicação impacta bastante a arquitetura.

Erro 5: não conectar MCP ao tool calling

Um dos maiores ganhos é justamente integrar as tools MCP ao fluxo do ChatClient.


7.32 Resumo do capítulo

Neste capítulo, você aprendeu que o Model Context Protocol (MCP) é uma forma padronizada de conectar modelos de IA a ferramentas, recursos e prompts externos. Vimos que:

  • o MCP funciona como uma ponte padronizada entre modelos e sistemas externos;
  • o Spring AI pode atuar tanto como cliente quanto como servidor MCP;
  • o cliente MCP pode consumir tools, resources e prompts;
  • o servidor MCP pode expor capabilities reutilizáveis;
  • tools MCP podem ser integradas ao mecanismo de tool calling do Spring AI;
  • o protocolo ajuda a reduzir acoplamento e melhorar interoperabilidade em cenários mais complexos.

7.33 Exercícios propostos

1. Conceitual
Explique com suas palavras a diferença entre tool calling e MCP.

2. Conceitual
Qual a diferença entre cliente MCP e servidor MCP?

3. Prático
Escreva um exemplo de configuração YAML para um cliente MCP via streamable-http.

4. Prático
Crie uma classe com @McpTool para somar dois números.

5. Reflexão
Em que cenário você escolheria @Tool local em vez de MCP?


Desafio extra

Esboce (em pseudocódigo ou projeto mínimo) um servidor MCP que exponha uma tool horaServidor() e consuma essa tool em um ChatClient com toolcallback.enabled=true. Documente qual transporte você usou (STDIO ou HTTP) e por quê.


7.34 Fechamento

Com este capítulo, você passa a enxergar uma camada ainda mais madura das aplicações com IA. Agora você entende que não basta ter tools locais; também é possível trabalhar com um protocolo padronizado para descobrir e consumir capacidades externas.

Esse capítulo prepara muito bem o terreno para o próximo passo: RAG — Retrieval Augmented Generation, em que o modelo responde com base em contexto recuperado de documentos e fontes externas.


Capítulo 8 — RAG no Spring AI: fazendo a IA responder com base em contexto recuperado

Objetivo do capítulo

Neste capítulo, você vai aprender:

  • o que é RAG;
  • por que RAG é um dos padrões mais importantes em aplicações com IA;
  • quais problemas ele resolve;
  • como funciona o fluxo de recuperação de contexto;
  • o que é um VectorStore;
  • como usar o QuestionAnswerAdvisor;
  • como aplicar filtros dinâmicos na busca;
  • como personalizar o template usado no enriquecimento do prompt;
  • como funciona o RetrievalAugmentationAdvisor;
  • como pensar RAG de forma arquitetural e não apenas como “mais um recurso”.

Ao final, você deve entender esta ideia:

quando o modelo precisa responder com base em conhecimento específico, atualizado ou interno da aplicação, RAG costuma ser muito mais adequado do que confiar apenas no conhecimento do modelo.


8.1 Introdução

Até aqui, trabalhamos com prompts, parâmetros, modelos, multimodalidade, tools e MCP. Em todos esses capítulos, ficou claro que a IA pode ser muito útil dentro de uma aplicação.

Mas existe um problema importante:

o modelo não sabe automaticamente tudo o que a sua empresa, sistema ou documento interno sabe.

Mesmo um modelo poderoso pode ter limitações em situações como:

  • responder com base em documentos internos;
  • lidar com políticas atualizadas recentemente;
  • consultar textos longos demais para o prompt bruto;
  • manter precisão factual em conhecimento muito específico;
  • responder sobre dados que não fizeram parte do treinamento.

É aqui que entra o RAG.


8.2 O que é RAG?

RAG significa Retrieval Augmented Generation.

Em linguagem simples

RAG é uma estratégia em que, antes de pedir a resposta ao modelo, a aplicação:

  1. recebe a pergunta do usuário;
  2. busca documentos ou trechos relevantes em uma base externa;
  3. injeta esse contexto recuperado no prompt;
  4. só então pede ao modelo que responda.
sequenceDiagram autonumber actor U as Usuário participant B as Backend(Spring AI) participant V as VectorStore participant M as Modelo de IA U->>B: Faz uma pergunta B->>V: Busca trechos similares (embedding da pergunta) V-->>B: Retorna documentos relevantes B->>B: Injeta contexto no prompt B->>M: Envia prompt enriquecido com contexto M-->>B: Gera resposta baseada no contexto B-->>U: Entrega resposta fundamentada

Ou seja, o modelo não responde apenas com base no que ele “já sabe”. Ele responde com base em contexto recuperado na hora.

Ideia principal

Sem RAG:

  • usuário pergunta;
  • modelo responde com base em treinamento + prompt atual.

Com RAG:

  • usuário pergunta;
  • sistema busca conteúdo relevante;
  • modelo responde com base nesse conteúdo + pergunta.

Essa diferença é enorme do ponto de vista de confiabilidade e de valor entregue ao usuário.


8.3 Por que RAG é tão importante?

Porque muitos problemas reais não se resolvem apenas com prompt bom.

Exemplos:

  • chatbot de empresa com FAQ interno;
  • assistente jurídico com base documental;
  • sistema escolar com regulamentos e material didático;
  • portal de suporte com base de conhecimento;
  • aplicação médica com protocolos internos;
  • sistema financeiro com regras do negócio;
  • help desk com documentação operacional.

Se você tentar resolver isso apenas com prompt manual, surgem vários problemas:

  • contexto insuficiente;
  • respostas genéricas;
  • risco de alucinação;
  • limitação de tamanho do prompt;
  • dificuldade de manter o conhecimento atualizado.

RAG resolve esse problema trazendo conhecimento relevante no momento da pergunta.


8.4 Qual problema o RAG resolve de verdade?

Você precisa entender isso com clareza.

RAG não existe apenas para “melhorar a resposta”.
Ele existe para resolver três problemas centrais:

1. Conhecimento específico

O modelo pode não ter sido treinado com aquele conteúdo.

2. Conhecimento atualizado

O documento pode ter sido criado depois do treinamento do modelo.

3. Conhecimento grande demais

Mesmo que você tenha o conteúdo, talvez ele seja grande demais para sempre mandar inteiro no prompt.


8.5 Exemplo intuitivo: sem RAG vs com RAG

Cenário

Você tem uma escola e quer responder:

“Qual é a política de reposição de aulas do curso de programação?”

Sem RAG

O modelo pode:

  • inventar uma política genérica;
  • responder com algo plausível, mas errado;
  • dizer algo que parece profissional, mas não veio do seu regulamento real.

Com RAG

A aplicação:

  • busca o trecho correto do regulamento;
  • injeta esse trecho no contexto;
  • pede que o modelo responda com base nesse conteúdo.

Resultado:

  • mais precisão;
  • mais aderência ao documento;
  • menos alucinação;
  • mais segurança operacional.

8.6 Como o fluxo de RAG funciona?

O fluxo básico de RAG pode ser explicado assim:

  1. o usuário faz uma pergunta;
  2. a pergunta é transformada em algo pesquisável;
  3. o sistema busca documentos semelhantes em uma base vetorial;
  4. os trechos encontrados são anexados ao contexto;
  5. o modelo recebe:
    • a pergunta;
    • o contexto recuperado;
  6. o modelo gera a resposta.

8.7 O que é um VectorStore?

Para RAG funcionar bem, normalmente precisamos de uma base especializada em busca semântica. No Spring AI, essa abstração aparece como VectorStore.

Em linguagem simples

Um VectorStore é um repositório preparado para armazenar documentos em forma vetorial e recuperar os trechos mais semanticamente parecidos com a pergunta do usuário.

Por que isso é diferente de uma busca comum por palavra?

Porque a ideia não é apenas procurar texto idêntico.
A ideia é encontrar conteúdo semanticamente relacionado.

Exemplo:

Pergunta do usuário:

Como funciona a reposição de aula?

Mesmo que o documento tenha escrito:

Você poderá remarcar a aula prática em caso de ausência justificada.

uma busca vetorial pode entender que há relação entre esses textos, mesmo sem palavra exata igual.


8.8 O Spring AI suporta RAG de que formas?

O Spring AI suporta RAG de duas formas principais:

  • com fluxos prontos via Advisor API;
  • com arquitetura modular para montar fluxos customizados.

Para este ebook, isso é excelente

Porque podemos ensinar RAG em dois níveis:

Nível 1 — mais simples

Usar um advisor pronto, como:

  • QuestionAnswerAdvisor

Nível 2 — mais avançado

Montar fluxos mais completos com:

  • RetrievalAugmentationAdvisor
  • retriever
  • augmenter
  • filtros
  • template customizado

8.9 Dependência para usar advisors com vector store

Para usar QuestionAnswerAdvisor e outros advisors ligados a vector store, você precisará das dependências apropriadas no projeto.

Exemplo de dependência

<dependency>
   <groupId>org.springframework.ai</groupId>
   <artifactId>spring-ai-advisors-vector-store</artifactId>
</dependency>

O que isso significa?

Que o suporte a esses advisors pode exigir um módulo específico além do starter básico.


8.10 Primeiro caminho: QuestionAnswerAdvisor

Esse é o caminho mais simples e mais didático para começar.

Exemplo básico

ChatResponse response = ChatClient.builder(chatModel)
        .build()
        .prompt()
        .advisors(QuestionAnswerAdvisor.builder(vectorStore).build())
        .user(userText)
        .call()
        .chatResponse();

O que esse código faz?

  1. cria um ChatClient;
  2. adiciona o QuestionAnswerAdvisor;
  3. envia a pergunta do usuário;
  4. o advisor consulta o VectorStore;
  5. os documentos encontrados são anexados ao contexto;
  6. o modelo responde com base nesse contexto.

8.11 Explicando o QuestionAnswerAdvisor de forma prática

Imagine que você tenha uma base de conhecimento com documentos já carregados no banco vetorial.

Quando o usuário pergunta:

Quais cursos têm aula na sexta?

o QuestionAnswerAdvisor:

  • busca os trechos mais relevantes sobre grade de aulas;
  • junta esse conteúdo com a pergunta;
  • envia tudo ao modelo.

O modelo então responde com muito mais chance de aderir ao conteúdo real.

O que você precisa guardar?

O advisor funciona como uma camada automática de enriquecimento de contexto.


8.12 Configurando threshold e quantidade de resultados

O QuestionAnswerAdvisor pode ser configurado com um SearchRequest, incluindo:

  • similarityThreshold
  • topK

Exemplo

var qaAdvisor = QuestionAnswerAdvisor.builder(vectorStore)
        .searchRequest(
                SearchRequest.builder()
                        .similarityThreshold(0.8d)
                        .topK(6)
                        .build()
        )
        .build();

O que isso significa?

similarityThreshold(0.8d)
Define o nível mínimo de similaridade para considerar um documento relevante.

topK(6)
Define que queremos recuperar os 6 resultados mais relevantes.

Por que isso importa?

Porque RAG não é só “buscar qualquer coisa”.
É importante controlar a qualidade do contexto recuperado.


8.13 O perigo de contexto demais ou contexto de menos

Se você recuperar contexto de menos:

  • a resposta pode ficar incompleta;
  • o modelo pode não ter base suficiente.

Se você recuperar contexto demais:

  • o prompt pode ficar poluído;
  • o modelo pode perder foco;
  • custo e latência podem aumentar;
  • contexto irrelevante pode atrapalhar a resposta.

Por isso, parâmetros como topK e similarityThreshold são tão importantes.


8.14 Filtros dinâmicos na busca

Um recurso muito poderoso em RAG é a possibilidade de filtrar dinamicamente os documentos buscados.

Exemplo

ChatClient chatClient = ChatClient.builder(chatModel)
    .defaultAdvisors(
        QuestionAnswerAdvisor.builder(vectorStore)
            .searchRequest(SearchRequest.builder().build())
            .build()
    )
    .build();

String content = chatClient.prompt()
    .user("Please answer my question XYZ")
    .advisors(a -> a.param(QuestionAnswerAdvisor.FILTER_EXPRESSION, "type == 'Spring'"))
    .call()
    .content();

O que isso significa?

Você pode restringir a busca vetorial em tempo real.

Exemplo prático de escola

Imagine que sua base vetorial tem documentos de várias áreas:

  • programação;
  • informática básica;
  • robótica;
  • design.

Você pode filtrar em runtime para buscar apenas documentos de programação.

Isso deixa o RAG muito mais preciso.


8.15 Por que filtros dinâmicos são tão valiosos?

Porque muitas aplicações têm base de conhecimento heterogênea.

Exemplos:

  • documentos por setor;
  • conteúdo por idioma;
  • dados por organização ou tenant;
  • FAQs por módulo ou linha de negócio;
  • políticas por unidade.

Se você não filtrar, a busca pode recuperar contexto irrelevante.

Com filtro, você reduz ruído e melhora a precisão da resposta.


8.16 Personalizando o template do contexto recuperado

O QuestionAnswerAdvisor pode usar um template customizado para combinar:

  • a pergunta do usuário;
  • o contexto recuperado.

Exemplo didático

PromptTemplate customPromptTemplate = PromptTemplate.builder()
    .template("""
            {query}

            Context information is below.

            ---------------------
            {question_answer_context}
            ---------------------

            Given the context information and no prior knowledge, answer the query.

            Follow these rules:
            1. If the answer is not in the context, just say that you don't know.
            2. Do not invent information.
            """)
    .build();

Por que isso é poderoso?

Porque RAG não é só recuperação de contexto.
Também importa como esse contexto é entregue ao modelo.


8.17 Exemplo prático com template customizado

QuestionAnswerAdvisor qaAdvisor = QuestionAnswerAdvisor.builder(vectorStore)
        .promptTemplate(customPromptTemplate)
        .build();

String response = ChatClient.builder(chatModel)
        .build()
        .prompt("Quais são as regras de reposição de aula?")
        .advisors(qaAdvisor)
        .call()
        .content();

O que esse exemplo mostra?

  • você usa RAG;
  • mas controla como o contexto entra no prompt;
  • e ainda impõe regras explícitas contra alucinação.

Isso é excelente para aplicações corporativas.


8.18 O que é o RetrievalAugmentationAdvisor?

Depois do caminho mais simples, existe uma abordagem mais modular chamada RetrievalAugmentationAdvisor.

O que isso quer dizer?

Se o QuestionAnswerAdvisor é uma porta de entrada mais simples, o RetrievalAugmentationAdvisor já aponta para uma visão mais arquitetural do RAG.


8.19 Exemplo de Naive RAG com RetrievalAugmentationAdvisor

Exemplo

Advisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder()
        .documentRetriever(
                VectorStoreDocumentRetriever.builder()
                        .similarityThreshold(0.50)
                        .vectorStore(vectorStore)
                        .build()
        )
        .build();

String answer = chatClient.prompt()
        .advisors(retrievalAugmentationAdvisor)
        .user(question)
        .call()
        .content();

O que isso faz?

  • define um document retriever;
  • liga esse retriever a um vector store;
  • configura o advisor de RAG;
  • usa o advisor no ChatClient.

O que é “naive RAG”?

É a forma mais direta:

  • recuperar documentos relevantes;
  • anexar contexto;
  • pedir resposta ao modelo.

8.20 O que acontece quando não há contexto recuperado?

Por padrão, em fluxos mais rigorosos de RAG, o sistema pode optar por não responder quando não houver contexto suficiente.

Por quê?

Porque uma armadilha comum em IA é deixar o modelo responder mesmo sem contexto suficiente.

Em um sistema sensível, isso pode aumentar o risco de:

  • alucinação;
  • resposta genérica;
  • falsa segurança.

8.21 Permitindo contexto vazio

Em alguns cenários, pode fazer sentido permitir contexto vazio.

Exemplo

Advisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder()
        .documentRetriever(
                VectorStoreDocumentRetriever.builder()
                        .similarityThreshold(0.50)
                        .vectorStore(vectorStore)
                        .build()
        )
        .queryAugmenter(
                ContextualQueryAugmenter.builder()
                        .allowEmptyContext(true)
                        .build()
        )
        .build();

Quando isso pode fazer sentido?

Quando você quer que o modelo ainda tente responder mesmo sem recuperação relevante.

Quando eu teria cuidado?

Em aplicações empresariais, isso pode ser perigoso se a resposta precisa estar estritamente fundamentada na base.


8.22 QuestionAnswerAdvisor ou RetrievalAugmentationAdvisor?

Use QuestionAnswerAdvisor quando:

  • você quer começar rápido;
  • quer um fluxo mais simples;
  • precisa de um RAG direto e fácil de entender.

Use RetrievalAugmentationAdvisor quando:

  • você quer mais modularidade;
  • pretende evoluir o fluxo de recuperação;
  • precisa de uma arquitetura mais extensível;
  • está pensando em RAG de forma mais avançada.

8.23 Exemplo completo de serviço com QuestionAnswerAdvisor

Aqui vai um exemplo didático consolidado.

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.client.advisor.QuestionAnswerAdvisor;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Service;

@Service
public class AssistenteRagService {

    private final ChatClient chatClient;
    private final VectorStore vectorStore;

    public AssistenteRagService(ChatModel chatModel, VectorStore vectorStore) {
        this.chatClient = ChatClient.builder(chatModel).build();
        this.vectorStore = vectorStore;
    }

    public String responder(String pergunta) {
        var qaAdvisor = QuestionAnswerAdvisor.builder(vectorStore)
                .searchRequest(
                        SearchRequest.builder()
                                .similarityThreshold(0.8d)
                                .topK(5)
                                .build()
                )
                .build();

        return chatClient.prompt()
                .system("Responda apenas com base no contexto recuperado. Se não souber, diga que não sabe.")
                .user(pergunta)
                .advisors(qaAdvisor)
                .call()
                .content();
    }
}

O que esse exemplo ensina?

  • uso de VectorStore;
  • enriquecimento de pergunta com contexto;
  • threshold e topK;
  • instrução explícita contra invenção.

8.24 Exemplo de controller para usar o RAG

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/faq")
public class FaqController {

    private final AssistenteRagService assistenteRagService;

    public FaqController(AssistenteRagService assistenteRagService) {
        this.assistenteRagService = assistenteRagService;
    }

    @GetMapping
    public String responder(@RequestParam String pergunta) {
        return assistenteRagService.responder(pergunta);
    }
}

Exemplo de chamada

GET /faq?pergunta=Como funciona a reposição de aula?

Fluxo esperado

  1. pergunta chega;
  2. busca vetorial roda;
  3. trechos relevantes são recuperados;
  4. o modelo responde com base nesses trechos.

8.25 Em que tipo de aplicação o RAG faz mais sentido?

Exemplos clássicos

1. FAQ inteligente
Baseado em regulamentos, perguntas frequentes, contratos ou políticas.

2. Base de conhecimento interna
Manuais, processos, playbooks e documentação da empresa.

3. Assistente de documentação interna
Funcionalidades do sistema, termos de uso, políticas e procedimentos da organização.

4. Portal educacional
Conteúdo de curso, regras acadêmicas, material de apoio.

5. Assistente jurídico ou financeiro
Quando o sistema precisa responder com base em documentos normativos específicos.


8.26 RAG não substitui tudo

Também é importante não tratar RAG como solução mágica.

RAG ajuda muito, mas ainda exige atenção em pontos como:

  • qualidade dos documentos carregados;
  • chunking e ingestão;
  • relevância da recuperação;
  • filtros;
  • qualidade do prompt final;
  • cuidado com contexto excessivo.

8.27 O que você deve guardar deste capítulo?

Mais do que decorar classes, ele precisa entender estas ideias:

  1. o modelo sozinho não basta para conhecimento específico e atualizado;
  2. RAG injeta contexto recuperado antes da resposta;
  3. o VectorStore é parte central dessa arquitetura;
  4. o QuestionAnswerAdvisor é a forma mais simples de começar;
  5. o RetrievalAugmentationAdvisor leva a um RAG mais modular;
  6. filtros e templates importam muito;
  7. RAG é uma estratégia arquitetural, não só um detalhe de API.

8.28 Boas práticas desta etapa

1. Sempre diga ao modelo como agir quando o contexto não for suficiente

Exemplo:

  • “Se não souber, diga que não sabe.”

2. Ajuste topK e similarityThreshold

Evite contexto demais e contexto irrelevante.

3. Use filtros quando houver múltiplos tipos de documentos

Isso melhora precisão.

4. Personalize o template quando o domínio exigir mais controle

Especialmente para ambientes corporativos.

5. Não deixe o modelo responder livremente quando o objetivo for resposta fundamentada

Esse é exatamente o tipo de cenário em que RAG existe.

6. Comece pelo QuestionAnswerAdvisor

E evolua para RetrievalAugmentationAdvisor quando precisar de mais modularidade.


8.29 Erros comuns nesta etapa

Erro 1: achar que RAG é só “jogar documento no prompt”

Não é. Há recuperação, seleção e augmentação de contexto.

Erro 2: recuperar documentos demais

Isso aumenta ruído.

Erro 3: não dizer ao modelo o que fazer quando a resposta não estiver no contexto

Isso aumenta alucinação.

Erro 4: não usar filtro em bases heterogêneas

Isso reduz precisão.

Erro 5: achar que qualquer busca é RAG

RAG envolve recuperação relevante e uso desse contexto na geração.


8.30 Resumo do capítulo

Neste capítulo, você aprendeu que RAG é uma técnica usada para melhorar respostas com base em contexto recuperado. Vimos que:

  • o modelo sozinho pode não ter conhecimento específico, atualizado ou suficiente para responder corretamente;
  • RAG recupera conteúdo relevante antes de pedir a resposta ao modelo;
  • o VectorStore é central nesse processo;
  • QuestionAnswerAdvisor é uma forma simples de começar;
  • RetrievalAugmentationAdvisor permite uma arquitetura mais modular;
  • filtros dinâmicos e templates customizados ajudam muito na qualidade da resposta;
  • RAG é uma estratégia fundamental para aplicações reais baseadas em documentos e conhecimento interno.

8.31 Exercícios propostos

1. Conceitual
Explique com suas palavras a diferença entre uma resposta comum do modelo e uma resposta com RAG.

2. Conceitual
Por que RAG ajuda a reduzir alucinação em aplicações com base documental?

3. Prático
Monte um exemplo com QuestionAnswerAdvisor usando topK(5) e similarityThreshold(0.8).

4. Prático
Crie um filtro dinâmico para buscar apenas documentos do tipo "programacao".

5. Reflexão
Em que cenário você preferiria QuestionAnswerAdvisor e em que cenário escolheria RetrievalAugmentationAdvisor?


Desafio extra

Carregue dois conjuntos fictícios no VectorStore (ex.: “políticas 2024” e “políticas 2025”) e use filtro dinâmico para que a mesma pergunta recupere só um dos conjuntos. Mostre a pergunta, o filtro e um trecho do contexto recuperado.


8.32 Fechamento

Com este capítulo, fechamos uma sequência muito forte deste ebook.

Agora você já viu:

  • integração básica;
  • parâmetros;
  • modelos;
  • erros e retry;
  • multimodalidade;
  • tool calling;
  • MCP;
  • e RAG.

Isso forma uma base muito sólida para começar a construir aplicações com Spring AI que sejam:

  • úteis;
  • organizadas;
  • integráveis;
  • menos acopladas;
  • mais próximas de cenários reais.