Kihagyás

Spring AI - Paralell Multi Agent

pom.xml

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>2.0.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

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

Config

@Bean
@Qualifier("businessChatClient")
ChatClient businessChatClient(ChatClient.Builder builder) {
    return builder
        .defaultSystem("""
            You are a business analyst.

            Analyze:
            - target users
            - business model
            - monetization
            - competition
            """)
        .build();
}

@Bean
@Qualifier("technicalChatClient")
ChatClient technicalChatClient(ChatClient.Builder builder) {
    return builder
        .defaultSystem("""
            You are a software architect.

            Analyze:
            - architecture
            - technology stack
            - scalability
            - implementation
            """)
        .build();
}

@Bean
@Qualifier("riskChatClient")
ChatClient riskChatClient(ChatClient.Builder builder) {
    return builder
        .defaultSystem("""
            You are a risk analyst.

            Analyze:
            - business risks
            - legal risks
            - technical risks
            - mitigations
            """)
        .build();
}

@Bean
@Qualifier("synthesizerChatClient")
ChatClient synthesizerChatClient(ChatClient.Builder builder) {
    return builder
        .defaultSystem("""
            You are a senior product strategist.

            Merge all agent outputs into one final answer.

            Create a well-structured report.
            Remove duplicates.
            Keep the most important findings.
            """)
        .build();
}

@Bean
ExecutorService agentExecutor() {
    return Executors.newVirtualThreadPerTaskExecutor();
}

application.yaml

# IMPORTANT: currently we have 1 test model, but in case of more than one, this needs to be setup per client from the code (OpenAiApi api = OpenAiApi...)
spring:
  ai:
    openai:
      base-url: https://example.com/
      api-key: shhh
      chat:
        options:
          model: Qwen3.6-35B-A3B-UD-Q4_K_XL.gguf

AgentLikeATool

@Service
public class BusinessAgent {

    private final ChatClient chatClient;

    public BusinessAgent(
        @Qualifier("businessChatClient")
        ChatClient chatClient) {

        this.chatClient = chatClient;
    }

    public String analyze(String question) {

        System.out.println("BUSINESS START");

        String result = chatClient.prompt()
            .user(question)
            .call()
            .content();

        System.out.println("BUSINESS END");

        return result;
    }
}

@Service
public class RiskAgent {

    private final ChatClient chatClient;

    public RiskAgent(
        @Qualifier("riskChatClient")
        ChatClient chatClient) {

        this.chatClient = chatClient;
    }

    public String analyze(String question) {

        System.out.println("RISK START");

        String result = chatClient.prompt()
            .user(question)
            .call()
            .content();

        System.out.println("RISK END");

        return result;
    }
}

@Service
public class TechnicalAgent {

    private final ChatClient chatClient;

    public TechnicalAgent(
        @Qualifier("technicalChatClient")
        ChatClient chatClient) {

        this.chatClient = chatClient;
    }

    public String analyze(String question) {

        System.out.println("TECHNICAL START");

        String result = chatClient.prompt()
            .user(question)
            .call()
            .content();

        System.out.println("TECHNICAL END");

        return result;
    }
}

@Service
public class SynthesizerAgent {

    private final ChatClient chatClient;

    public SynthesizerAgent(
        @Qualifier("synthesizerChatClient")
        ChatClient chatClient) {

        this.chatClient = chatClient;
    }

    public String synthesize(
        String originalQuestion,
        String business,
        String technical,
        String risk) {

        return chatClient.prompt()
            .user("""
                    Original question:
                    %s

                    Business analysis:
                    %s

                    Technical analysis:
                    %s

                    Risk analysis:
                    %s
                    """.formatted(
                originalQuestion,
                business,
                technical,
                risk))
            .call()
            .content();
    }
}

@Service
public class MultiAgentOrchestrator {

    private final BusinessAgent businessAgent;
    private final TechnicalAgent technicalAgent;
    private final RiskAgent riskAgent;
    private final SynthesizerAgent synthesizerAgent;
    private final ExecutorService executor;

    public MultiAgentOrchestrator(
        BusinessAgent businessAgent,
        TechnicalAgent technicalAgent,
        RiskAgent riskAgent,
        SynthesizerAgent synthesizerAgent,
        ExecutorService executor) {

        this.businessAgent = businessAgent;
        this.technicalAgent = technicalAgent;
        this.riskAgent = riskAgent;
        this.synthesizerAgent = synthesizerAgent;
        this.executor = executor;
    }

    public String process(String question) {

        long start = System.currentTimeMillis();

        CompletableFuture<String> businessFuture =
            CompletableFuture.supplyAsync(
                () -> businessAgent.analyze(question),
                executor);

        CompletableFuture<String> technicalFuture =
            CompletableFuture.supplyAsync(
                () -> technicalAgent.analyze(question),
                executor);

        CompletableFuture<String> riskFuture =
            CompletableFuture.supplyAsync(
                () -> riskAgent.analyze(question),
                executor);

        CompletableFuture.allOf(
            businessFuture,
            technicalFuture,
            riskFuture
        ).join();

        System.out.println("ALL AGENTS FINISHED");

        String result = synthesizerAgent.synthesize(
            question,
            businessFuture.join(),
            technicalFuture.join(),
            riskFuture.join()
        );

        System.out.printf("TOTAL TIME: %s ms%n", System.currentTimeMillis() - start);

        return result;
    }
}

ChatController

@RestController
public class ChatController {

    private final MultiAgentOrchestrator orchestrator;

    public ChatController(
        MultiAgentOrchestrator orchestrator) {

        this.orchestrator = orchestrator;
    }

    @GetMapping("/chat")
    public String chat(@RequestParam String message) {
        return orchestrator.process(message);
    }
}

Curl

curl --get --data-urlencode "message=Design a SaaS platform for dog owners" localhost:8080/chat