Building AI Agents with Spring AI Framework: A Practical Guide
The rise of Large Language Models (LLMs) has fundamentally changed how we approach software development. But while LLMs are powerful, they’re inherently stateless and limited to text generation. To build truly useful applications, we need AI agents — autonomous systems that can reason, use tools, and interact with the world.
Enter Spring AI, the Spring ecosystem’s answer to integrating AI capabilities into enterprise Java applications. As a seasoned Java developer who has built several production AI systems, I can confidently say Spring AI provides the most pragmatic framework for building AI agents in the Java ecosystem.
In this guide, I’ll walk you through building AI agents with Spring AI, from basic concepts to production-ready multi-agent systems. We’ll cover real code, architectural patterns, and the lessons I’ve learned the hard way.
Why Spring AI for AI Agents?
Before diving into code, let’s understand why Spring AI stands out:
Familiar Spring paradigms: If you know Spring Boot, you already know 80% of Spring AI. It uses the same dependency injection, configuration, and abstraction patterns.
Vendor independence: Switch between OpenAI, Anthropic, Ollama, or Azure OpenAI by changing a single property. No code changes needed.
Enterprise readiness: Built-in retry logic, observability with Micrometer, and seamless integration with Spring’s transaction management.
Tool ecosystem: First-class support for function calling, RAG (Retrieval-Augmented Generation), and vector databases.
Setting Up Your Spring AI Project
Let’s start with a minimal Spring Boot project. Add the following to your pom.xml:
publicSimpleAgent(ChatClient.Builder builder) { this.chatClient = builder .defaultSystem(""" You are a helpful assistant with access to tools. Use the provided tools to answer questions accurately. If you cannot find the answer, say so. """) .defaultFunctions("getCurrentTime", "getWeather") .build(); }
Now define the tools (functions) the agent can use:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
@Component @Description("Get the current time for a given timezone") publicclassGetCurrentTimeFunctionimplementsFunction<GetCurrentTimeFunction.Request, GetCurrentTimeFunction.Response> {
publicrecordRequest(String timezone) {} publicrecordResponse(String time, String timezone) {}
@Component @Description("Get the current weather for a location") publicclassGetWeatherFunctionimplementsFunction<GetWeatherFunction.Request, GetWeatherFunction.Response> {
@Override public Response apply(Request request) { // In production, call a real weather API returnnewResponse("22°C", "Sunny"); } }
The magic happens through function calling. The LLM decides when to call which function based on the user’s query. Spring AI handles the serialization, invocation, and response integration automatically.
Memory and Conversation History
Stateless agents are useless for real conversations. Spring AI provides ChatMemory implementations:
spring: ai: chat: memory: type:redis# or 'in-memory' for dev
Advanced Agent Patterns
ReAct Agents with Spring AI
The ReAct (Reasoning + Acting) pattern is the gold standard for complex agents. Spring AI implements this via ToolCallback and a custom prompt template:
privatestaticfinalStringREACT_PROMPT=""" Answer the following questions as best you can. You have access to the following tools: {tools} Use the following format: Question: the input question Thought: you should always think about what to do Action: the tool to take (one of {tool_names}) Action Input: the input to the action Observation: the result of the action ... (this Thought/Action/Action Input/Observation can repeat N times) Thought: I now know the final answer Final Answer: the final answer to the original input question Question: {input} Thought: """;
publicMultiAgentOrchestrator( Agent researchAgent, Agent writingAgent, Agent factCheckAgent, ChatClient.Builder builder) { this.researchAgent = researchAgent; this.writingAgent = writingAgent; this.factCheckAgent = factCheckAgent; this.router = builder .defaultSystem(""" You are a router agent. Determine which specialist agent should handle the user's request: - For research questions: route to 'research' - For content creation: route to 'writing' - For verification: route to 'factcheck' Respond with only the agent name. """) .build(); }
public String handleRequest(String request) { StringagentName= router.prompt() .user(request) .call() .content();
publicRagAgent(ChatClient.Builder builder, VectorStore vectorStore) { this.vectorStore = vectorStore; this.chatClient = builder .defaultSystem(""" You are a knowledgeable assistant. Use the provided context to answer questions accurately. If the context doesn't contain relevant information, say so. """) .defaultAdvisors( newVectorStoreChatMemoryAdvisor(vectorStore) ) .build(); }
public String ask(String question) { return chatClient.prompt() .user(question) .advisors(a -> a .param("chat_memory_conversation_id", "session-1") .param("chat_memory_response_size", 3)) .call() .content(); } }
Configure a vector store (e.g., PostgreSQL with pgvector):
@Test voidtestTimeQuery() { Stringresponse= agent.ask("What time is it in London?"); assertThat(response).contains("2024"); assertThat(response).contains("London"); }
@Test voidtestToolUsage() { // Verify the agent actually calls tools when(weatherService.getWeather(any())).thenReturn(newWeather("25°C", "Cloudy")); Stringresponse= agent.ask("What's the weather in Paris?"); assertThat(response).contains("25°C"); verify(weatherService).getWeather(eq("Paris")); } }
Real-World Architecture
Here’s a production-ready architecture I’ve used successfully:
API Gateway: Rate limiting, authentication, request validation
Agent Router: Determines which specialist agent to invoke
Specialist Agents: Focused agents for specific domains (code, docs, data)
Vector Store: Long-term memory and RAG context
Tool Executor: Async tool execution with timeout and retry
Common Pitfalls and Solutions
Token limit exceeded: Use ChatClient with maxTokens and chunking for large documents
Tool hallucination: Always validate tool inputs with explicit schemas
Conversation drift: Implement periodic summarization of conversation history
Cost explosion: Set per-request token limits and monitor aggressively
Latency: Use streaming responses (chatClient.prompt().stream()) for better UX
Key Takeaways
Spring AI provides a production-ready foundation for building AI agents in Java, leveraging familiar Spring patterns like dependency injection and auto-configuration.
Function calling is the backbone of agent capabilities — define tools as Spring beans and let the LLM decide when to use them.
Memory management is critical for conversational agents; use ChatMemory implementations for conversation history and vector stores for long-term knowledge.
Multi-agent architectures scale better than monolithic agents; use a router agent to delegate to specialists.
Observability and cost management are non-negotiable in production — leverage Micrometer metrics and set strict token limits.
Testing AI agents requires a shift from deterministic assertions to behavior verification and integration tests with mock LLM responses.
Building AI agents with Spring AI feels like cheating — it handles all the complex orchestration while letting you focus on what makes your application unique. The framework is maturing rapidly, and I expect it to become the standard for Java-based AI development.