Spring AI 2.0 RC1 tool calling - migrar para ToolCallingAdvisor - Meu Universo Nerd

Você atualizou a versão do Spring AI, subiu pra homologação e o agente simplesmente parou de chamar as ferramentas. Sem erro. Sem stack trace. Sem nada nos logs. O modelo responde como se as tools nunca tivessem existido, e você acabou de gastar a tarde procurando bug no lugar errado.

Esse cenário vai ser comum quando o Spring AI 2.0 chegar ao GA. O RC1, lançado em 6 de junho de 2026, removeu o loop interno de execução de tools de todos os ChatModels. Se o seu código ainda registra ferramentas por nome com toolNames(), elas vão virar fantasma. Nesse artigo você vai ver exatamente o que mudou e como migrar antes que o GA te pegue desprevenido.

Você atualizou a versão do Spring AI, subiu pra homologação e o agente simplesmente parou de chamar as ferramentas. Sem erro. Sem stack trace. Sem nada nos logs. O modelo responde como se as tools nunca tivessem existido, e você acabou de gastar a tarde procurando bug no lugar errado.

Esse cenário vai ser comum quando o Spring AI 2.0 chegar ao GA. O RC1, lançado em 6 de junho de 2026, removeu o loop interno de execução de tools de todos os ChatModels. Se o seu código ainda registra ferramentas por nome com toolNames(), elas vão virar fantasma. Nesse artigo você vai ver exatamente o que mudou e como migrar antes que o GA te pegue desprevenido.

Spoiler: não é difícil. Mas é silencioso, e é exatamente esse tipo de mudança que estoura em produção numa sexta às 18h. Bora destrinchar.

Como o tool calling funcionava até agora

Function calling (ou tool calling) é o que transforma um chat comum em agente. Você descreve uma ferramenta, o modelo decide quando chamar, o framework executa e devolve o resultado pro modelo continuar o raciocínio. Até o Spring AI 1.x, esse ciclo de chamar a tool, executar e reenviar a resposta acontecia dentro do próprio ChatModel.

Na prática, você registrava a ferramenta de duas formas: passava o bean por nome com toolNames(), ou deixava o SpringBeanToolCallbackResolver achar o bean pelo nome no contexto do Spring. O modelo cuidava do resto. Era cômodo, mas escondia o ciclo de execução num lugar onde você não conseguia interceptar nem testar direito.

@Configuration
public class AgenteConfig {

    // Jeito ANTIGO (Spring AI 1.x): tool resolvida por nome de bean
    @Bean
    public ChatClient chatClient(ChatClient.Builder builder) {
        return builder
            .defaultOptions(ToolCallingChatOptions.builder()
                .toolNames("buscarPedido", "consultarEstoque")
                .build())
            .build();
    }
}

Esse padrão é o que mais aparece em tutorial de Spring AI por aí. E é justamente ele que deixou de funcionar. Se quiser o contexto de como esse tema cai em processo seletivo, já falei sobre como o Spring AI aparece em entrevistas sênior em outro artigo.

O que o RC1 quebrou (e por que falha calado)

A mudança central do Spring AI 2.0.0-RC1 está descrita no anúncio oficial do release. Em uma frase: o loop de execução de tools saiu de dentro de todo ChatModel. A documentação é direta ao dizer que "the built-in call/stream tool-execution loop and ToolExecutionEligibilityChecker wiring have been dropped from every ChatModel", incluindo OpenAI, Anthropic, Ollama, MistralAI, DeepSeek, Bedrock Proxy e MiniMax.

Junto com o loop, foram embora as APIs de registro por nome. O toolNames e o toolBeanDefinitionNames foram removidos de ChatOptions, ToolCallingChatOptions, DefaultToolCallingManager e do próprio ChatClient. O SpringBeanToolCallbackResolver também saiu de cena.

Aqui está o detalhe perigoso. No RC1, o código que usa toolNames() nem sempre estoura na sua cara. Dependendo de como você montou o build, o método some na compilação (e aí você descobre cedo), ou a opção é ignorada em runtime e o modelo responde sem nunca chamar a ferramenta. Esse segundo caso é o pesadelo: a aplicação sobe, o endpoint responde 200, e só o resultado está errado. O agente "esqueceu" de consultar o estoque, mas ninguém reclamou em log nenhum.

Quem acompanhou a leva de breaking changes do Spring em 2026 já sabe o roteiro: o que é deprecation no RC vira remoção no GA. Ou seja, o tempo de migrar com calma é agora.

A migração: ToolCallback beans e ToolCallingAdvisor

O novo modelo é mais explícito e, sinceramente, melhor. Em vez de o ChatModel cuidar do ciclo escondido, você controla a execução de tools de fora, via ChatClient com um ToolCallingAdvisor. As ferramentas deixam de ser resolvidas por nome e passam a ser registradas como beans ToolCallback, entregues direto no .tools().

Olha como fica o mesmo agente, do jeito certo no 2.0:

@Configuration
public class AgenteConfig {

    // Cada ferramenta vira um ToolCallback explícito
    @Bean
    public ToolCallback buscarPedidoTool(PedidoService service) {
        return FunctionToolCallback
            .builder("buscarPedido", service::buscarPorId)
            .description("Busca um pedido pelo seu identificador")
            .inputType(Long.class)
            .build();
    }

    @Bean
    public ChatClient chatClient(ChatClient.Builder builder,
                                 ToolCallback buscarPedidoTool) {
        return builder
            // O advisor assume o ciclo que o ChatModel não faz mais
            .defaultAdvisors(ToolCallingAdvisor.builder().build())
            .defaultToolCallbacks(buscarPedidoTool)
            .build();
    }
}

No ponto da chamada, você também pode passar as tools sob demanda. O ChatClient.prompt().tools(...) agora aceita ToolCallback, ToolCallbackProvider, coleções e arrays direto:

@Service
public class AtendimentoService {

    private final ChatClient chatClient;
    private final ToolCallback buscarPedidoTool;

    // injeção via construtor, do jeito que os sêniores fazem
    public AtendimentoService(ChatClient chatClient,
                              ToolCallback buscarPedidoTool) {
        this.chatClient = chatClient;
        this.buscarPedidoTool = buscarPedidoTool;
    }

    public String responder(String pergunta) {
        return chatClient.prompt()
            .user(pergunta)
            .tools(buscarPedidoTool)   // passa o ToolCallback direto
            .call()
            .content();
    }
}

Repare numa sutileza importante: a ordem dos advisors. Se você usa memória de conversa, o ChatMemoryAdvisor precisa ficar posicionado fora do ciclo de tools, senão você duplica mensagens de ferramenta no histórico. O modelo novo deixa isso visível, e essa visibilidade é o ganho real. Você finalmente consegue testar o ciclo de execução de tool de forma isolada, sem subir um provider de verdade.

Por que tiraram o loop de dentro do modelo

À primeira vista parece que o time do Spring te deu mais trabalho de graça. Mas tem um motivo de arquitetura sólido por trás, e ele importa pra quem pensa em manutenção de longo prazo. A camada de ChatModel tinha duas responsabilidades misturadas: conversar com o provider (OpenAI, Anthropic, Ollama) e orquestrar o ciclo de execução de ferramentas. Isso é violação clássica de responsabilidade única, e cobrava o preço na hora de testar.

Com o loop morando dentro do modelo, pra testar "o agente chamou a tool certa?" você era obrigado a mockar o provider inteiro ou gastar tokens de verdade num teste. Agora o ciclo vive no ToolCallingAdvisor, que é um componente isolado e injetável. Dá pra testar a orquestração de tool sem encostar no modelo, e dá pra trocar a estratégia de execução sem reescrever o ChatModel.

Tem ainda o ganho de composição. Como tudo agora é advisor, o ciclo de tool entra na mesma cadeia de ChatMemoryAdvisor, logging, observabilidade e guard rails. Você enxerga a ordem, controla o que roda antes e depois, e para de depender de mágica escondida. Pra quem já apanhou tentando interceptar uma chamada de tool no 1.x pra logar ou auditar, essa mudança é libertadora. Pra quem precisa de controle total, ainda existe o caminho manual via DefaultToolCallingManager, em que você decide na unha quando executar a ferramenta e reenviar o resultado ao modelo.

E quando a resposta é em streaming?

Boa pergunta, porque a remoção pegou os dois caminhos: o anúncio fala explicitamente em "call/stream tool-execution loop". Ou seja, tanto o .call() quanto o .stream() perderam o ciclo embutido. Se o seu agente devolve resposta em streaming pro front (aquele efeito de texto aparecendo aos poucos), ele também precisa do advisor.

A boa notícia é que, com o ToolCallingAdvisor registrado nos defaults, o streaming continua simples. Você troca o .call() por .stream() e o advisor cuida de montar os pedaços de chamada de tool que chegam fatiados do provider:

// Mesmo advisor dos defaults, agora em streaming
Flux<String> fluxo = chatClient.prompt()
    .user(pergunta)
    .tools(buscarPedidoTool)
    .stream()
    .content();

O detalhe que pega muita gente: em streaming, a decisão do modelo de chamar uma tool chega em pedaços (chunks). Quem remonta esses pedaços e dispara a execução agora é o advisor, não o modelo. Se você tinha código caseiro tratando chunk de tool call na mão, pode jogar fora. Menos código seu, menos bug seu.

Dois presentes que vieram junto: ToolSearch e EntityParamSpec

O RC1 não foi só remoção. Veio também o ToolSearchToolCallingAdvisor, um advisor de descoberta de tools sob demanda. O problema que ele resolve é concreto: quando seu agente tem 50, 80, 100 ferramentas vindas de vários servidores MCP, mandar todas as definições pro modelo a cada chamada queima dezenas de milhares de tokens antes da conversa começar.

Com o ToolSearch, as ferramentas ficam num índice (há três implementações de ToolIndex, incluindo busca semântica via VectorStore e busca por palavra-chave) e só as relevantes pra pergunta atual são apresentadas ao modelo. O próprio time do Spring reportou economia de tokens expressiva em agentes com muitas tools. É o tipo de otimização que paga a conta no fim do mês quando você roda em escala.

O outro presente é o EntityParamSpec. Antes, pra forçar saída estruturada com validação de schema, você precisava montar advisor manual no builder. Agora dá pra configurar o structured output nativo do provider e a validação direto no ponto da chamada entity():

// structured output configurado por chamada, sem advisor manual
Pedido pedido = chatClient.prompt()
    .user("Extraia os dados do pedido desse texto")
    .call()
    .entity(EntityParamSpec.builder(Pedido.class)
        .useProviderNativeOutput(true)
        .validateSchema(true)
        .build());

Menos gambiarra, mais controle. É a direção certa pra quem leva agente Java pra produção de verdade.

Checklist de migração antes do GA

Se você mantém código com Spring AI hoje, esse é o roteiro pra não ser pego de surpresa:

  • Procure por toolNames( e toolBeanDefinitionNames( em todo o projeto. Cada ocorrência é um ponto de migração obrigatório.
  • Transforme cada tool em um bean ToolCallback, de preferência com FunctionToolCallback.builder(), com description clara (o modelo usa essa descrição pra decidir quando chamar).
  • Adicione um ToolCallingAdvisor nos defaults do ChatClient, ou registre as tools por chamada com .tools().
  • Revise a ordem dos advisors, principalmente se usa ChatMemoryAdvisor. Memória fora do ciclo de tools.
  • Teste o ciclo de tool isolado. Agora dá. Cubra o caso em que o modelo decide chamar a ferramenta e o caso em que ele decide não chamar.
  • Se você tem muitas tools, avalie o ToolSearchToolCallingAdvisor pra cortar custo de token.

Antes de tocar no código, vale ler as notas de upgrade oficiais do Spring AI, que listam item por item o que mudou de uma versão pra outra. É a sua fonte da verdade quando bater dúvida sobre algum método que sumiu.

Vale o mesmo conselho que dou pra qualquer breaking change grande, e que já apareceu na cobertura das mudanças recentes de performance da plataforma Java: migre num branch isolado, com os testes verdes antes e depois. Não tente fazer isso direto na main numa sexta.

O que isso muda na sua carreira

Tem um lado de mercado nisso que pouca gente conecta. Function calling parou de ser enfeite de demo e virou requisito em vaga de backend Java com IA. E a pergunta de entrevista mudou de nível junto. Não perguntam mais só "o que é uma tool". Perguntam "como você controla o ciclo de execução dela" e "como você testa isso sem chamar o provider".

Como Tech Leader, o que vejo é que quem entende esse novo modelo explícito do Spring AI 2.0 responde essas perguntas com naturalidade, enquanto quem só copiou tutorial do toolNames() trava. No mercado de hoje, dominar o ciclo de tool calling é diferencial real de quem constrói agente pra produção, não pra slide.

Perguntas frequentes

O Spring AI 2.0 já está em produção?
Não. O 2.0.0-RC1 saiu em 6 de junho de 2026 e é release candidate, o último degrau antes do GA. A ideia do RC é exatamente essa: te dar tempo de migrar antes da versão estável. Use o RC em ambiente de teste pra validar a migração, não direto em produção crítica.

Meu código com toolNames() vai parar de compilar ou de funcionar?
Depende. As APIs foram removidas, então em muitos casos a compilação acusa. Mas há cenários em que a opção é simplesmente ignorada em runtime, e aí o agente sobe sem chamar as tools. Por isso a recomendação é procurar ativamente por toolNames( no projeto, não esperar o compilador avisar.

Preciso reescrever todas as tools de uma vez?
Sim, na prática. Como o loop saiu de todos os ChatModels, não dá pra manter o modelo antigo em paralelo. A boa notícia é que a conversão é mecânica: cada tool antiga vira um bean ToolCallback. Em projetos pequenos é coisa de uma tarde.

O ToolCallingAdvisor é obrigatório?
Você precisa de algum mecanismo externo pra executar o ciclo de tools. O ToolCallingAdvisor é o caminho recomendado e mais simples. Para casos avançados existe o controle manual via DefaultToolCallingManager, mas pra 95% dos projetos o advisor resolve.

Vale a pena migrar agora ou esperar o GA?
Migre agora, num branch. O esforço é o mesmo, e fazer durante o RC te dá margem pra testar com calma. Esperar o GA só comprime o prazo e aumenta a chance de você descobrir o problema em produção.

Resumindo

  • O loop de tools saiu de todo ChatModel no Spring AI 2.0 RC1. toolNames() e SpringBeanToolCallbackResolver foram removidos.
  • A migração é registrar tools como beans ToolCallback e adicionar um ToolCallingAdvisor no ChatClient. Mais explícito, mais testável.
  • O risco real é a falha silenciosa: ferramenta ignorada sem erro em log. Procure por toolNames( no projeto antes do GA.

Você já roda agente com Spring AI em produção? Já tinha visto esse comportamento de tool sumir sem reclamar? Conta nos comentários como está a sua stack, quero saber quem já está no 2.0.

Na próxima, vou mostrar na prática como configurar o ToolSearchToolCallingAdvisor com VectorStore e medir a economia de token num agente com dezenas de ferramentas. Fica de olho aqui no Meu Universo Nerd.