Learn how to build autonomous AI agents that can plan, use tools, and accomplish complex tasks. This module covers agent architectures, tool integration, the Model Context Protocol (MCP), and creating intelligent systems that go beyond simple question-answering.
- Understand AI agent architectures and patterns
- Implement tool/function calling for AI agents
- Build agents that can plan and execute multi-step tasks
- Create and integrate MCP (Model Context Protocol) servers
- Implement agent memory and state management
- Design autonomous workflows with AI agents
- Handle agent errors and fallback strategies
- Build multi-agent systems
- What are AI Agents?
- Agent architectures: ReAct, Plan-and-Execute, Reflexion
- Tool/Function calling with Spring AI
- Model Context Protocol (MCP) overview
- Creating MCP servers and clients
- Agent memory and conversation state
- Planning and reasoning
- Multi-agent collaboration
- Agent orchestration patterns
- Error handling and recovery
- Memory and conversation history
- Enterprise integration patterns
- Retry strategies and fallbacks
- Domain-Driven Design with AI Agents
- Real-world agent use cases
AI Agents are autonomous systems that can:
- 🎯 Set Goals: Understand and pursue objectives
- 🔧 Use Tools: Call APIs, query databases, execute code
- 🧠 Reason: Plan multi-step solutions
- 💾 Remember: Maintain context across interactions
- 🔄 Iterate: Try different approaches if one fails
| Feature | Simple AI | AI Agent |
|---|---|---|
| Interaction | Single Q&A | Multi-step tasks |
| Tools | None | Can use many tools |
| Planning | None | Can create plans |
| Memory | None/Limited | Persistent memory |
| Autonomy | Low | High |
The agent alternates between reasoning and taking actions:
1. Think: "I need to find the weather"
2. Act: Call weather API
3. Observe: "Temperature is 72°F"
4. Think: "Now I can answer"
5. Answer: "It's 72°F today"
Agent creates a complete plan, then executes it:
Plan:
1. Get user's location
2. Call weather API
3. Format response
Execute each step...
Agent self-reflects and improves:
1. Attempt task
2. Evaluate success
3. If failed, reflect on why
4. Try again with improved approach
MCP is an open protocol that standardizes how AI applications connect to data sources and tools.
- ✅ Standardized: One protocol for all tools
- ✅ Reusable: Share MCP servers across applications
- ✅ Secure: Fine-grained access control
- ✅ Composable: Combine multiple MCP servers
┌──────────────┐ ┌──────────────┐
│ AI Agent │◀───────▶│ MCP Server │
│ (Client) │ MCP │ (Tools) │
└──────────────┘ └──────────────┘
│
┌──────┴──────┐
│ │
┌────▼───┐ ┌────▼───┐
│Database│ │ APIs │
└────────┘ └────────┘
@Component
public class WeatherTool implements Function<WeatherRequest, WeatherResponse> {
@Override
@Description("Get current weather for a location")
public WeatherResponse apply(
@JsonProperty(value = "location", required = true)
@JsonPropertyDescription("City name, e.g., 'London'")
WeatherRequest request
) {
// Call actual weather API
return weatherService.getWeather(request.location());
}
}
record WeatherRequest(String location) {}
record WeatherResponse(String location, double temperature, String condition) {}Register and use the tool:
@Service
public class AgentService {
private final ChatClient chatClient;
private final List<Function<?, ?>> tools;
public String executeWithTools(String userMessage) {
return chatClient.call(
new UserMessage(userMessage),
ChatOptions.builder()
.withFunctions(tools)
.build()
);
}
}@Service
public class ReActAgent {
private final ChatClient chatClient;
private final Map<String, Function<?, ?>> tools;
private static final int MAX_ITERATIONS = 5;
public String execute(String goal) {
List<Message> conversation = new ArrayList<>();
conversation.add(new SystemMessage(REACT_SYSTEM_PROMPT));
conversation.add(new UserMessage(goal));
for (int i = 0; i < MAX_ITERATIONS; i++) {
ChatResponse response = chatClient.call(
new Prompt(conversation,
ChatOptions.builder().withFunctions(tools.values()).build())
);
Message assistantMessage = response.getResult().getOutput();
conversation.add(assistantMessage);
// Check if agent is done
if (isDone(assistantMessage)) {
return extractFinalAnswer(assistantMessage);
}
// Execute any function calls
if (hasFunctionCall(assistantMessage)) {
String toolResult = executeTool(assistantMessage);
conversation.add(new UserMessage("Tool result: " + toolResult));
}
}
return "Agent exceeded maximum iterations";
}
private static final String REACT_SYSTEM_PROMPT = """
You are an AI agent that can use tools to accomplish tasks.
For each task, think step by step:
1. Thought: Reason about what to do next
2. Action: Use a tool if needed
3. Observation: Analyze the tool result
4. Repeat until you can answer
When you have the final answer, respond with:
Final Answer: [your answer]
""";
}@RestController
@RequestMapping("/mcp")
public class McpServerController {
private final DatabaseService databaseService;
// List available tools
@GetMapping("/tools")
public List<McpTool> listTools() {
return List.of(
new McpTool(
"query_database",
"Execute SQL query on the database",
new JsonSchema(/* schema for parameters */)
),
new McpTool(
"get_table_schema",
"Get schema information for a table",
new JsonSchema(/* schema */)
)
);
}
// Execute tool
@PostMapping("/execute")
public McpToolResult executeTool(@RequestBody McpToolRequest request) {
return switch (request.tool()) {
case "query_database" ->
queryDatabase(request.parameters());
case "get_table_schema" ->
getTableSchema(request.parameters());
default ->
throw new IllegalArgumentException("Unknown tool: " + request.tool());
};
}
private McpToolResult queryDatabase(Map<String, Object> params) {
String sql = (String) params.get("query");
List<Map<String, Object>> results = databaseService.executeQuery(sql);
return new McpToolResult(true, results, null);
}
}
record McpTool(String name, String description, JsonSchema parameters) {}
record McpToolRequest(String tool, Map<String, Object> parameters) {}
record McpToolResult(boolean success, Object data, String error) {}@Service
public class McpClient {
private final RestTemplate restTemplate;
private final String mcpServerUrl;
public List<McpTool> getAvailableTools() {
return restTemplate.exchange(
mcpServerUrl + "/mcp/tools",
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<McpTool>>() {}
).getBody();
}
public McpToolResult callTool(String toolName, Map<String, Object> parameters) {
McpToolRequest request = new McpToolRequest(toolName, parameters);
return restTemplate.postForObject(
mcpServerUrl + "/mcp/execute",
request,
McpToolResult.class
);
}
}@Service
public class AgentMemoryService {
private final Map<String, AgentMemory> sessions = new ConcurrentHashMap<>();
private final VectorStore vectorStore;
public void storeMemory(String sessionId, String content, String type) {
AgentMemory memory = sessions.computeIfAbsent(
sessionId,
k -> new AgentMemory()
);
// Short-term memory (conversation history)
memory.addToHistory(content);
// Long-term memory (vector store for retrieval)
if (type.equals("IMPORTANT")) {
Document doc = new Document(content);
doc.getMetadata().put("sessionId", sessionId);
doc.getMetadata().put("timestamp", Instant.now());
vectorStore.add(List.of(doc));
}
}
public String getRelevantMemory(String sessionId, String query) {
// Retrieve from vector store
List<Document> relevant = vectorStore.similaritySearch(
SearchRequest.query(query)
.withTopK(5)
.withFilterExpression(
Filter.expression("sessionId == '" + sessionId + "'")
)
);
return relevant.stream()
.map(Document::getContent)
.collect(Collectors.joining("\n\n"));
}
}
class AgentMemory {
private final Deque<String> conversationHistory = new ArrayDeque<>(100);
public void addToHistory(String content) {
if (conversationHistory.size() >= 100) {
conversationHistory.removeFirst();
}
conversationHistory.addLast(content);
}
public List<String> getHistory() {
return new ArrayList<>(conversationHistory);
}
}@Service
public class PlanningAgent {
private final ChatClient chatClient;
private final Map<String, Function<?, ?>> tools;
public String executeTask(String goal) {
// Step 1: Create plan
List<String> plan = createPlan(goal);
System.out.println("Plan: " + plan);
// Step 2: Execute each step
Map<String, Object> context = new HashMap<>();
for (int i = 0; i < plan.size(); i++) {
String step = plan.get(i);
System.out.println("Executing step " + (i+1) + ": " + step);
String result = executeStep(step, context);
context.put("step_" + i + "_result", result);
}
// Step 3: Synthesize final answer
return synthesizeFinalAnswer(goal, context);
}
private List<String> createPlan(String goal) {
String planPrompt = """
Create a step-by-step plan to accomplish this goal: %s
Available tools: %s
Provide a numbered list of steps.
Each step should be clear and actionable.
""".formatted(goal, getToolDescriptions());
String response = chatClient.call(planPrompt);
return parsePlan(response);
}
private String executeStep(String step, Map<String, Object> context) {
String prompt = """
Execute this step: %s
Context from previous steps: %s
Use tools if needed to complete the step.
""".formatted(step, context);
return chatClient.call(
new Prompt(prompt,
ChatOptions.builder().withFunctions(tools.values()).build())
);
}
}@Service
public class MultiAgentSystem {
private final Map<String, Agent> agents;
public MultiAgentSystem(
ResearchAgent researchAgent,
WriterAgent writerAgent,
ReviewerAgent reviewerAgent
) {
this.agents = Map.of(
"researcher", researchAgent,
"writer", writerAgent,
"reviewer", reviewerAgent
);
}
public String executeWorkflow(String task) {
// Agent 1: Research
String research = agents.get("researcher").execute(
"Research information about: " + task
);
// Agent 2: Write
String draft = agents.get("writer").execute(
"Write an article based on this research: " + research
);
// Agent 3: Review and improve
String finalArticle = agents.get("reviewer").execute(
"Review and improve this article: " + draft
);
return finalArticle;
}
}
interface Agent {
String execute(String task);
}- Code Assistant Agent: Understand requirements, write code, run tests, fix bugs
- Research Agent: Search multiple sources, synthesize information, cite sources
- Data Analysis Agent: Query databases, perform analysis, generate visualizations
- Customer Support Agent: Understand issue, search knowledge base, provide solution
- DevOps Agent: Monitor systems, diagnose issues, execute fixes
AI agents need to remember previous interactions for coherent conversations:
@Service
public class ConversationService {
private final ChatClient chatClient;
private final Map<String, MessageHistory> sessions = new ConcurrentHashMap<>();
public String chat(String sessionId, String userMessage) {
// Get or create conversation history
MessageHistory history = sessions.computeIfAbsent(
sessionId,
k -> new MessageHistory()
);
// Add user message
history.add(new UserMessage(userMessage));
// Get AI response with full history
String response = chatClient.prompt()
.messages(history.getMessages())
.call()
.content();
// Store AI response
history.add(new AssistantMessage(response));
return response;
}
public void clearHistory(String sessionId) {
sessions.remove(sessionId);
}
}class ConversationBuffer {
private final Deque<Message> messages = new ArrayDeque<>();
private final int maxMessages;
void add(Message message) {
messages.addLast(message);
if (messages.size() > maxMessages) {
messages.removeFirst(); // Keep only recent messages
}
}
}@Service
class AgentMemoryService {
private final VectorStore memoryStore;
void remember(String sessionId, String interaction) {
var document = new Document(
interaction,
Map.of("sessionId", sessionId, "timestamp", Instant.now())
);
memoryStore.add(List.of(document));
}
List<String> recall(String sessionId, String query) {
return memoryStore.similaritySearch(
SearchRequest.builder()
.query(query)
.topK(5)
.filterExpression("sessionId == '" + sessionId + "'")
.build()
).stream()
.map(Document::getContent)
.toList();
}
}class SummaryMemory {
private final ChatClient chatClient;
private String conversationSummary = "";
void updateSummary(List<Message> recentMessages) {
String messagesToSummarize = formatMessages(recentMessages);
conversationSummary = chatClient.prompt()
.system("""
Summarize the key points from this conversation.
Include important facts, decisions, and context.
Keep it concise but comprehensive.
""")
.user("Previous summary: " + conversationSummary +
"\n\nNew messages: " + messagesToSummarize)
.call()
.content();
}
}@Service
class AIEventProcessor {
private final ChatClient chatClient;
private final ApplicationEventPublisher eventPublisher;
@EventListener
public void handleBusinessEvent(OrderCreatedEvent event) {
// AI analyzes the order
var analysis = chatClient.prompt()
.user("Analyze this order for potential issues: " + event.getOrder())
.call()
.entity(OrderAnalysis.class);
if (analysis.hasPotentialFraud()) {
eventPublisher.publishEvent(new FraudAlertEvent(analysis));
}
}
}@RestController
@RequestMapping("/ai-service")
class AIServiceController {
private final AgentService agentService;
@PostMapping("/analyze")
public ResponseEntity<AnalysisResult> analyze(
@RequestBody AnalysisRequest request,
@RequestHeader("X-API-Key") String apiKey
) {
// Validate API key
if (!isValidApiKey(apiKey)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
// Rate limiting
if (rateLimiter.isLimitExceeded(apiKey)) {
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).build();
}
var result = agentService.analyze(request);
return ResponseEntity.ok(result);
}
}@Service
class AsyncAIProcessor {
@RabbitListener(queues = "ai-requests")
public void processRequest(AIRequest request) {
try {
var result = chatClient.prompt()
.user(request.getPrompt())
.call()
.content();
// Publish result
rabbitTemplate.convertAndSend("ai-results",
new AIResult(request.getId(), result));
} catch (Exception e) {
// Send to dead letter queue
rabbitTemplate.convertAndSend("ai-requests-dlq", request);
}
}
}@Service
class CachedAIService {
@Cacheable(value = "ai-responses", key = "#prompt")
public String getCachedResponse(String prompt) {
return chatClient.prompt()
.user(prompt)
.call()
.content();
}
// Cache semantic similarity
@Cacheable(value = "semantic-cache", keyGenerator = "semanticKeyGenerator")
public String getSemanticallyCachedResponse(String prompt) {
// Check if semantically similar prompt exists in cache
// Uses vector similarity to find matches
return chatClient.prompt().user(prompt).call().content();
}
}@Service
class ResilientAIService {
@CircuitBreaker(name = "ai-service", fallbackMethod = "fallbackResponse")
@Retry(name = "ai-service", fallbackMethod = "fallbackResponse")
public String getResponse(String prompt) {
return chatClient.prompt()
.user(prompt)
.call()
.content();
}
private String fallbackResponse(String prompt, Exception e) {
log.error("AI service failed, using fallback", e);
return "I'm experiencing technical difficulties. Please try again later.";
}
}Configuration in application.yaml:
resilience4j:
circuitbreaker:
instances:
ai-service:
failure-rate-threshold: 50
wait-duration-in-open-state: 60s
sliding-window-size: 10
retry:
instances:
ai-service:
max-attempts: 3
wait-duration: 2s
exponential-backoff-multiplier: 2@Service
class TimeoutAwareAIService {
public String getResponseWithTimeout(String prompt) {
try {
return CompletableFuture.supplyAsync(() ->
chatClient.prompt().user(prompt).call().content()
).get(30, TimeUnit.SECONDS);
} catch (TimeoutException e) {
log.warn("AI request timed out after 30s");
return "Response took too long. Try a simpler query.";
}
}
}@Service
class DegradableAIService {
private final ChatClient primaryModel;
private final ChatClient fallbackModel;
public String getResponse(String prompt) {
try {
// Try primary (larger, better) model
return primaryModel.prompt()
.user(prompt)
.call()
.content();
} catch (Exception e) {
log.warn("Primary model failed, using fallback", e);
try {
// Fall back to smaller, faster model
return fallbackModel.prompt()
.user(prompt)
.call()
.content();
} catch (Exception e2) {
log.error("Both models failed", e2);
// Final fallback: rule-based response
return getRuleBasedResponse(prompt);
}
}
}
}@Service
class SafeAIService {
private final ChatClient chatClient;
private final ContentModerationService moderationService;
public String getSafeResponse(String prompt) {
// Pre-validation
if (moderationService.isUnsafeInput(prompt)) {
throw new UnsafeContentException("Input contains unsafe content");
}
var response = chatClient.prompt()
.user(prompt)
.call()
.content();
// Post-validation
if (moderationService.isUnsafeOutput(response)) {
log.warn("Unsafe AI response detected, filtering");
return "I cannot provide that information.";
}
return response;
}
}// Customer Support Context
@Service
class CustomerSupportAgent {
private final ChatClient supportSpecialist;
@PostConstruct
void init() {
// This agent is fine-tuned for customer support
supportSpecialist.setSystemPrompt("""
You are a customer support specialist for TechCorp.
Be empathetic, solution-oriented, and follow company policies.
""");
}
}
// Sales Context
@Service
class SalesAgent {
private final ChatClient salesSpecialist;
@PostConstruct
void init() {
// Different agent, different expertise
salesSpecialist.setSystemPrompt("""
You are a sales assistant for TechCorp.
Be persuasive, highlight product benefits, and close deals.
""");
}
}@Entity
class CustomerProfile {
@Id
private Long id;
private String preferences;
private List<Interaction> history;
// Domain logic enhanced by AI
public ProductRecommendation getRecommendation(AIAgent agent) {
String context = buildContextFromHistory();
return agent.recommend(context, this.preferences);
}
}@DomainEvents
class Order {
public List<Object> registerEvents(AIAgent agent) {
List<Object> events = new ArrayList<>();
// AI analyzes order and generates events
var analysis = agent.analyzeOrder(this);
if (analysis.requiresApproval()) {
events.add(new OrderRequiresApprovalEvent(this));
}
if (analysis.suggestsUpsell()) {
events.add(new UpsellOpportunityEvent(this, analysis.getSuggestions()));
}
return events;
}
}@RestController
@RequestMapping("/mcp")
class RecipeMCPServer {
private final RecipeService recipeService;
// MCP tool definition
@PostMapping("/tools/list")
public MCPToolsResponse listTools() {
return MCPToolsResponse.builder()
.tools(List.of(
MCPTool.builder()
.name("search_recipes")
.description("Search for recipes by ingredients")
.inputSchema(Map.of(
"type", "object",
"properties", Map.of(
"ingredients", Map.of(
"type", "array",
"items", Map.of("type", "string"),
"description", "List of ingredients"
)
),
"required", List.of("ingredients")
))
.build(),
MCPTool.builder()
.name("get_recipe_details")
.description("Get detailed recipe information")
.inputSchema(Map.of(
"type", "object",
"properties", Map.of(
"recipeId", Map.of(
"type", "string",
"description", "The recipe ID"
)
)
))
.build()
))
.build();
}
// MCP tool execution
@PostMapping("/tools/call")
public MCPToolResponse callTool(@RequestBody MCPToolRequest request) {
return switch (request.getName()) {
case "search_recipes" -> {
var ingredients = (List<String>) request.getArguments().get("ingredients");
var recipes = recipeService.search(ingredients);
yield MCPToolResponse.success(recipes);
}
case "get_recipe_details" -> {
var recipeId = (String) request.getArguments().get("recipeId");
var recipe = recipeService.getById(recipeId);
yield MCPToolResponse.success(recipe);
}
default -> MCPToolResponse.error("Unknown tool: " + request.getName());
};
}
// MCP resources (for RAG)
@PostMapping("/resources/list")
public MCPResourcesResponse listResources() {
return MCPResourcesResponse.builder()
.resources(List.of(
MCPResource.builder()
.uri("recipe://database/all")
.name("Recipe Database")
.description("All recipes in the system")
.mimeType("application/json")
.build()
))
.build();
}
}@Service
class MCPAwareAgent {
private final RestTemplate mcpClient;
private final ChatClient chatClient;
public Recipe findRecipe(String userQuery) {
// 1. Discover available tools
var tools = mcpClient.postForObject(
"http://mcp-server/mcp/tools/list",
null,
MCPToolsResponse.class
);
// 2. AI decides which tool to use
var decision = chatClient.prompt()
.system("You have access to these tools: " + tools)
.user("User wants: " + userQuery)
.call()
.entity(ToolDecision.class);
// 3. Call the MCP tool
var result = mcpClient.postForObject(
"http://mcp-server/mcp/tools/call",
new MCPToolRequest(decision.toolName(), decision.arguments()),
MCPToolResponse.class
);
// 4. Format response for user
return chatClient.prompt()
.user("Format this data for the user: " + result.getContent())
.call()
.entity(Recipe.class);
}
}@Service
class MultiAgentCoordinator {
private final List<MCPServer> mcpServers;
private final ChatClient orchestrator;
public String handleComplexQuery(String query) {
// Each agent exposes its capabilities via MCP
var allTools = mcpServers.stream()
.flatMap(server -> server.listTools().stream())
.toList();
// Orchestrator decides task breakdown
var plan = orchestrator.prompt()
.system("Available tools: " + allTools)
.user("Create a plan to answer: " + query)
.call()
.entity(ExecutionPlan.class);
// Execute plan across multiple agents
var results = new HashMap<String, Object>();
for (var step : plan.getSteps()) {
var server = findServerForTool(step.getTool());
var result = server.callTool(step.getTool(), step.getArguments());
results.put(step.getName(), result);
}
// Synthesize final answer
return orchestrator.prompt()
.user("Synthesize answer from: " + results)
.call()
.content();
}
}- Set Clear Goals: Agents work best with specific, measurable objectives
- Limit Iterations: Prevent infinite loops with max iteration counts
- Handle Errors: Tools can fail; implement fallbacks
- Log Everything: Track agent decisions for debugging
- Cost Control: Monitor token usage carefully
- Human in the Loop: For critical decisions, require human approval
- Test Thoroughly: Agent behavior can be unpredictable
- Start Simple: Begin with single-tool agents before building complex systems
- Use MCP: Standardize tool interfaces for reusability
- Implement Memory: Stateless agents are limited; add memory for context
- Monitor Performance: Track success rates, latency, and costs
- Validate Inputs/Outputs: Never trust AI blindly
Complete code examples will be provided in the workshop repository.
Congratulations on completing the workshop! You now have the knowledge to build secure, scalable AI applications with self-hosted LLMs, autonomous agents, proper error handling, and enterprise integration patterns.