AgentClient vs ChatClient

Spring AI Agents follows the same design patterns as Spring AI’s ChatClient to provide a familiar experience for Spring developers. This page shows the similarities and differences between the two APIs.

1. API Comparison

1.1. Basic Usage

Both APIs follow the same fluent pattern for simple interactions:

ChatClient AgentClient
String response = chatClient.prompt("What is Spring?").call().getResult().getOutput().getContent();
String response = agentClient.run("Create a Hello World class").getResult();

1.2. Fluent Configuration

Both support fluent configuration with method chaining:

ChatClient AgentClient
String response = chatClient.prompt("Explain Spring Boot")
    .options(ChatOptionsBuilder.builder()
        .withModel("gpt-4")
        .withMaxTokens(1000)
        .build())
    .call()
    .getResult()
    .getOutput()
    .getContent();
String response = agentClient.goal("Refactor UserService")
    .workingDirectory("/path/to/project")
    .options(ClaudeCodeAgentOptions.builder()
        .model("claude-sonnet-4-0")
        .maxTokens(8192)
        .build())
    .run()
    .getResult();

2. Key Differences

2.1. Purpose and Scope

ChatClient AgentClient

Conversational AI - Text-in, text-out interactions - Stateless request/response model - No external tool access - Immediate responses

Autonomous Goal Execution - Goal-oriented execution - Multi-step workflows with planning - Full tool access (files, commands, web) - Variable execution time

2.2. Input/Output Model

ChatClient AgentClient

Messages and Prompts

ChatResponse response = chatClient
    .prompt("How do I create a REST API?")
    .call();

String content = response
    .getResult()
    .getOutput()
    .getContent();

Goals and Results

AgentClientResponse response = agentClient
    .run("Create a REST API for User management");

String result = response.getResult();
boolean successful = response.isSuccessful();

2.3. Configuration Options

ChatClient AgentClient

Model Parameters

ChatOptions options = ChatOptionsBuilder.builder()
    .withModel("gpt-4")
    .withTemperature(0.7f)
    .withMaxTokens(1000)
    .build();

chatClient.prompt("Generate code")
    .options(options)
    .call();

Execution Environment

ClaudeCodeAgentOptions options =
    ClaudeCodeAgentOptions.builder()
        .model("claude-sonnet-4-0")
        .timeout(Duration.ofMinutes(10))
        .build();

agentClient.goal("Generate code")
    .workingDirectory("/project/path")
    .yolo(true)
    .options(options)
    .run();

3. Spring Boot Integration

Both integrate seamlessly with Spring Boot through auto-configuration:

3.1. Configuration Classes

ChatClient Configuration AgentClient Configuration
@Configuration
public class ChatConfiguration {

    @Bean
    public ChatClient chatClient(ChatModel chatModel) {
        return ChatClient.create(chatModel);
    }
}
@Configuration
public class AgentConfiguration {

    @Bean
    public AgentClient agentClient(AgentModel agentModel) {
        return AgentClient.create(agentModel);
    }
}

3.2. Controller Usage

ChatClient in Controller AgentClient in Controller
@RestController
public class ChatController {

    private final ChatClient chatClient;

    public ChatController(ChatClient chatClient) {
        this.chatClient = chatClient;
    }

    @PostMapping("/ask")
    public String ask(@RequestBody String question) {
        return chatClient.prompt(question)
            .call()
            .getResult()
            .getOutput()
            .getContent();
    }
}
@RestController
public class TaskController {

    private final AgentClient agentClient;

    public TaskController(AgentClient agentClient) {
        this.agentClient = agentClient;
    }

    @PostMapping("/execute")
    public String execute(@RequestBody String goal) {
        AgentClientResponse response = agentClient.run(goal);
        return response.getResult();
    }
}

4. Response Handling

4.1. Success/Failure Patterns

ChatClient AgentClient
ChatResponse response = chatClient.prompt("Explain Java").call();

String content = response
    .getResult()
    .getOutput()
    .getContent();
AgentClientResponse response = agentClient.run("Fix compilation errors");

if (response.isSuccessful()) {
    System.out.println("Goal completed: " + response.getResult());
} else {
    System.err.println("Goal failed: " + response.getResult());
}

4.2. Metadata Access

ChatClient AgentClient
ChatResponse response = chatClient.prompt("Hello").call();

Generation result = response.getResult();
ChatGenerationMetadata metadata = result.getMetadata();

String finishReason = metadata.getFinishReason();
Usage usage = metadata.getUsage();
AgentClientResponse response = agentClient.run("Create tests");

AgentResponseMetadata metadata = response.getMetadata();

Duration executionTime = metadata.getDuration();
String agentModel = metadata.getModel();

5. When to Use Each

5.1. Use ChatClient When:

  • Conversational interactions - Q&A, explanations, content generation

  • Text processing - Summarization, translation, analysis

  • Immediate responses - Real-time chat, quick queries

  • Stateless operations - Each request is independent

Example use cases:

// Content generation
String blogPost = chatClient.prompt("Write a blog post about Spring Security").call()...;

// Data analysis
String summary = chatClient.prompt("Summarize this sales report: " + data).call()...;

// Q&A
String answer = chatClient.prompt("How do I configure Spring Data JPA?").call()...;

5.2. Use AgentClient When:

  • Goal execution - Code generation, refactoring, debugging

  • Multi-step workflows - Complex development tasks requiring planning

  • Tool interaction - File manipulation, command execution, web research

  • Project-specific work - Tasks requiring codebase understanding

Example use cases:

// Code generation
agentClient.run("Create a REST controller for User entity with CRUD operations");

// Debugging
agentClient.run("Find and fix the memory leak in the OrderService class");

// Refactoring
agentClient.run("Convert this project from JUnit 4 to JUnit 5");

6. Migration Between APIs

While the APIs are similar in structure, they serve different purposes. You typically won’t migrate between them, but rather choose the appropriate one for each use case:

@Service
public class DevelopmentAssistantService {

    private final ChatClient chatClient;
    private final AgentClient agentClient;

    public DevelopmentAssistantService(ChatClient chatClient, AgentClient agentClient) {
        this.chatClient = chatClient;
        this.agentClient = agentClient;
    }

    // Use ChatClient for explanations
    public String explainPattern(String patternName) {
        return chatClient.prompt("Explain the " + patternName + " design pattern").call()
            .getResult().getOutput().getContent();
    }

    // Use AgentClient for implementation
    public String implementPattern(String patternName, String context) {
        return agentClient.run("Implement " + patternName + " pattern in " + context)
            .getResult();
    }
}

7. Best Practices

7.1. Consistent Error Handling

Both APIs benefit from consistent error handling patterns:

@Component
public class AIService {

    public String askQuestion(String question) {
        try {
            return chatClient.prompt(question).call()
                .getResult().getOutput().getContent();
        } catch (Exception e) {
            log.error("Chat request failed: {}", e.getMessage());
            throw new ServiceException("Unable to process question", e);
        }
    }

    public String executeTask(String goal) {
        try {
            AgentClientResponse response = agentClient.run(goal);
            if (response.isSuccessful()) {
                return response.getResult();
            } else {
                throw new GoalExecutionException("Goal failed: " + response.getResult());
            }
        } catch (Exception e) {
            log.error("Goal execution failed: {}", e.getMessage());
            throw new ServiceException("Unable to execute goal", e);
        }
    }
}

7.2. Resource Management

Both clients are thread-safe and designed for reuse:

@Configuration
public class AIConfiguration {

    // Both clients are singleton beans - thread-safe and efficient
    @Bean
    public ChatClient chatClient(ChatModel chatModel) {
        return ChatClient.create(chatModel);
    }

    @Bean
    public AgentClient agentClient(AgentModel agentModel) {
        return AgentClient.create(agentModel);
    }
}

8. Advisor Pattern Comparison

Both ChatClient and AgentClient support the same advisor pattern for intercepting and augmenting execution flows. This pattern is one of the most powerful similarities between the two APIs.

8.1. What are Advisors?

Advisors provide interception points that let you inject custom logic before and after AI model calls. They’re perfect for:

  • Adding context (RAG data for ChatClient, workspace info for AgentClient)

  • Logging and metrics collection

  • Security validation and filtering

  • Post-processing and evaluation

8.2. Advisor Registration

Both APIs use identical builder patterns for registering advisors:

ChatClient Advisors AgentClient Advisors
ChatClient client = ChatClient.builder(chatModel)
    .defaultAdvisor(new MessageChatMemoryAdvisor())
    .defaultAdvisor(new QuestionAnswerAdvisor())
    .build();
AgentClient client = AgentClient.builder(agentModel)
    .defaultAdvisor(new WorkspaceContextAdvisor())
    .defaultAdvisor(new TestExecutionAdvisor())
    .build();

8.3. Advisor Implementation

The advisor interface follows the same around-style advice pattern in both APIs:

ChatClient Advisor AgentClient Advisor
public class LoggingAdvisor
    implements CallAroundAdvisor {

    @Override
    public ChatResponse aroundCall(
        ChatRequest request,
        CallAroundAdvisorChain chain) {

        // Before model call
        log.info("Prompt: {}",
            request.getPrompt());

        // Execute
        ChatResponse response =
            chain.nextCall(request);

        // After model call
        log.info("Response: {}",
            response.getResult());

        return response;
    }
}
public class LoggingAdvisor
    implements AgentCallAdvisor {

    @Override
    public AgentClientResponse adviseCall(
        AgentClientRequest request,
        AgentCallAdvisorChain chain) {

        // Before agent execution
        log.info("Goal: {}",
            request.goal());

        // Execute
        AgentClientResponse response =
            chain.nextCall(request);

        // After agent execution
        log.info("Result: {}",
            response.getResult());

        return response;
    }
}

8.4. Use Case Comparison

While the pattern is identical, the specific use cases differ based on the domain:

ChatClient Advisor Use Cases AgentClient Advisor Use Cases

Context Augmentation

  • RAG: Inject retrieved documents

  • Chat memory: Add conversation history

  • Prompt engineering: Template expansion

Post-Processing

  • Response filtering

  • Content moderation

  • Output formatting

Observability

  • Token usage tracking

  • Latency monitoring

  • Cost calculation

Context Engineering

  • Git repository cloning

  • Dependency synchronization (vendir)

  • Workspace preparation

  • Project metadata gathering

Post-Execution Evaluation (Judges)

  • Test suite execution

  • File existence verification

  • Code quality checks

  • Schema validation

Observability

  • Execution time tracking

  • File modification metrics

  • Command execution logging

8.5. Spring Boot Auto-Configuration

Both support automatic discovery of advisor beans:

ChatClient Auto-Configuration AgentClient Auto-Configuration
@Configuration
public class ChatAdvisorConfig {

    @Bean
    public CallAroundAdvisor memoryAdvisor() {
        return new MessageChatMemoryAdvisor();
    }

    @Bean
    public ChatClient chatClient(
        ChatModel model,
        List<CallAroundAdvisor> advisors) {
        return ChatClient.builder(model)
            .defaultAdvisors(advisors)
            .build();
    }
}
@Configuration
public class AgentAdvisorConfig {

    @Bean
    public AgentCallAdvisor contextAdvisor() {
        return new WorkspaceContextAdvisor();
    }

    @Bean
    public AgentClient agentClient(
        AgentModel model,
        List<AgentCallAdvisor> advisors) {
        return AgentClient.builder(model)
            .defaultAdvisors(advisors)
            .build();
    }
}

8.6. Context Sharing Between Advisors

Both APIs provide mutable context maps for advisors to share data:

ChatClient Context AgentClient Context
public class WriterAdvisor
    implements CallAroundAdvisor {

    @Override
    public ChatResponse aroundCall(
        ChatRequest request,
        CallAroundAdvisorChain chain) {

        // Write to context
        request.context()
            .put("user_id", getCurrentUser());

        return chain.nextCall(request);
    }
}

public class ReaderAdvisor
    implements CallAroundAdvisor {

    @Override
    public ChatResponse aroundCall(
        ChatRequest request,
        CallAroundAdvisorChain chain) {

        // Read from context
        String userId = (String) request
            .context().get("user_id");

        return chain.nextCall(request);
    }
}
public class WriterAdvisor
    implements AgentCallAdvisor {

    @Override
    public AgentClientResponse adviseCall(
        AgentClientRequest request,
        AgentCallAdvisorChain chain) {

        // Write to context
        request.context()
            .put("workspace_info", analyze());

        return chain.nextCall(request);
    }
}

public class ReaderAdvisor
    implements AgentCallAdvisor {

    @Override
    public AgentClientResponse adviseCall(
        AgentClientRequest request,
        AgentCallAdvisorChain chain) {

        // Read from context
        String info = (String) request
            .context().get("workspace_info");

        return chain.nextCall(request);
    }
}

8.7. Advisor Ordering

Both use Spring’s Ordered interface for execution order control:

// Same pattern for both ChatClient and AgentClient advisors
public class SecurityAdvisor implements AgentCallAdvisor {

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE; // Run first
    }
}

public class MetricsAdvisor implements AgentCallAdvisor {

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE; // Run last
    }
}

8.8. Learn More About Advisors

The advisor pattern demonstrates the deep architectural alignment between AgentClient and ChatClient, making it easy to apply familiar Spring AI patterns to autonomous agent development.

9. Summary

AgentClient and ChatClient share the same Spring AI design philosophy and fluent API patterns, making them familiar to Spring developers. The key difference is their purpose: ChatClient for conversational AI interactions, AgentClient for autonomous goal execution.

Both APIs integrate seamlessly into Spring Boot applications and follow established Spring patterns for configuration, dependency injection, and error handling. The advisor pattern is nearly identical between the two, providing powerful extension points for both conversational and autonomous AI workflows.

10. Next Steps