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