Build Smarter Java Agents: Multi-Model AI Orchestration with Quarkus and LangChain4j
Use local LLMs, spawn multiple agents dynamically, and orchestrate complex AI workflows with Quarkus in Java.
In this tutorial, you will build a fully functional Quarkus application that demonstrates a powerful AI pattern: an "instructor" agent generating instructions on the fly for two newly created "worker" agent. This allows for dynamic, context-specific task execution.
We will build a simple REST endpoint that accepts a high-level goal. This goal will be sent to a "instructor" AI, which will generate detailed steps. These steps will then be embedded as the system prompt for two brand new, programmatically created "worker" AI, which will then execute the task.
Prerequisites
Before you begin, ensure you have the following installed:
JDK 17+: Check with
java -version
.Apache Maven 3.8+: Check with
mvn -version
.Podman: To run Ollama and a local language model.
And as usual: If you don’t want to walk through this step by step, feel free to grab the working example from my Github repository.
Running a Local LLM with Ollama
The simplest way to get a local Large Language Model (LLM) running is with Ollama. You can chose between two ways. Either natively installed Ollama or with lama.cpp running in a Dev Services container. Quarkus finds a locally installed Ollama. If you want to, you can install it. But you don’t have to.
Install Ollama: Follow the official instructions at ollama.com.
Pull the Models: We'll use phi3
, a capable and lightweight model from Microsoft. And gemma:2b a more creative model from Google.
Open your terminal and run:
ollama pull phi3
ollama pull gemma:2b
Run Ollama: The Ollama service typically runs in the background after installation. If you need to start it manually or want to see its logs, you can run ollama serve
. By default, it serves the API on http://localhost:11434
3. Project Setup
We'll use the Quarkus Maven plugin to create our project.
Generate the Project: Open your terminal and run the following command.
mvn io.quarkus.platform:quarkus-maven-plugin:3.23.3:create \
-DprojectGroupId=org.acme \
-DprojectArtifactId=dynamic-agent-spawner \
-Dextensions="rest-jackson,langchain4j-ollama"
cd dynamic-agent-spawner
Your project is now set up with Quarkus REST for the REST endpoint and the Langchain4j Ollama integration.
Application Configuration
Tell Quarkus how to connect to our local Ollama instance. And tell it about the models we are going to use.
Open src/main/resources/application.properties
and add the following configuration:
# 1. DEFAULT MODEL (for logical tasks)
quarkus.langchain4j.ollama.chat-model.model-id=phi3
# Optional but reccomended: Set a timeout
quarkus.langchain4j.ollama.timeout=60s
#quarkus.langchain4j.ollama.log-requests=true
# 2. NAMED "CREATIVE" MODEL (for writing tasks)
quarkus.langchain4j.ollama.creative.chat-model.model-id=gemma:2b
quarkus.langchain4j.ollama.creative.timeout=120s
#quarkus.langchain4j.ollama.creative.log-requests=true
Creating the Agent Interfaces
Now, let's define the Java interfaces for our two agents.
a) The "Worker" Agent Interface
This is the interface for the agent we will spawn dynamically. Its system message is a template that we will populate at runtime.
Create a new file src/main/java/org/acme/TaskExecutionAgent.java
:
package org.acme;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
public interface TaskExecutionAgent {
@SystemMessage("""
You are a specialized agent, an expert in following instructions.
Your instructions are as follows:
---
{{instructions}}
---
Execute the task based ONLY on these instructions.
""")
String executeTask(@UserMessage String details, String instructions);
}
{{instructions}}
: This is a template variable. Langchain4j will replace it with the value we provide when calling the method.
b) The "Instructor" Agent Interface
This is the agent that generates the instructions. We'll register it as a standard Quarkus bean using @RegisterAiService
.
Create a new file src/main/java/org/acme/InstructionGeneratorAgent.java
:
package org.acme;
import dev.langchain4j.service.UserMessage;
import io.quarkiverse.langchain4j.RegisterAiService;
@RegisterAiService // No change here
public interface InstructionGeneratorAgent {
@UserMessage("""
You are a project manager. A user wants to achieve a goal.
Your job is to create two distinct sets of instructions for two different AI agents: a PLANNER and a WRITER.
The PLANNER agent's job is to create a structured outline or a list of key points.
The WRITER agent's job is to take the planner's output and write the final, polished text.
Respond with ONLY a valid JSON object that adheres to the following structure, with no preamble:
{
"plannerInstructions": "Instructions for the AI planner...",
"writerInstructions": "Instructions for the AI writer..."
}
The user's goal is: {{goal}}
""")
// Langchain4j will automatically parse the LLM's JSON output into this object.
DecomposedInstructions generateInstructions(String goal);
}
You have noticed the return type here. As we are using two “Agents” in this scenario, one of them is going to be the planner and the other the creative agent. We need instructions for both. We map them with a Java record:
Create a new file src/main/java/org/acme/DecomposedInstructions.java
:
package org.acme;
// This Record models the JSON structure we want from the LLM
public record DecomposedInstructions(
String plannerInstructions,
String writerInstructions
) {}
Implementing the "Spawner" Service
This is the core orchestrator. It uses the InstructionGeneratorAgent
to get instructions and then uses the AiServices
builder to programmatically create and run the TaskExecutionAgent
.
Create a new file src/main/java/org/acme/AgentSpawner.java
:
package org.acme;
import org.jboss.logging.Logger;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.service.AiServices;
import io.quarkiverse.langchain4j.ModelName;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@ApplicationScoped
public class AgentSpawner {
@Inject
InstructionGeneratorAgent instructionGenerator;
// Inject the default model (phi3)
@Inject
ChatModel defaultModel;
// Inject the model named "creative" (gemma:2b)
@Inject
@ModelName("creative")
ChatModel creativeModel;
private static final Logger LOG = Logger.getLogger(AgentSpawner.class);
public String spawnAndExecute(String highLevelGoal) {
// 1. Generate the decomposed instructions as a structured object
LOG.infof("Generating decomposed instructions for goal: %s", highLevelGoal);
DecomposedInstructions instructions = instructionGenerator.generateInstructions(highLevelGoal);
LOG.infof("Generated Planner Instructions: %s", instructions.plannerInstructions());
LOG.infof("Generated Writer Instructions: %s", instructions.writerInstructions());
// 2. Spawn Agent 1 (Planner) with the default (phi3) model
LOG.infof("Spawning PLANNER agent with model: %s", defaultModel.toString());
TaskExecutionAgent plannerAgent = AiServices.builder(TaskExecutionAgent.class)
.chatModel(defaultModel)
.build();
String plan = plannerAgent.executeTask(highLevelGoal, instructions.plannerInstructions());
LOG.info("Planner Agent Output (The Plan):\n" + plan);
// 3. Spawn Agent 2 (Writer) with the "creative" (gemma:2b) model
LOG.infof("Spawning WRITER agent with model: %s", creativeModel.toString());
TaskExecutionAgent writerAgent = AiServices.builder(TaskExecutionAgent.class)
.chatModel(creativeModel)
.build();
// We use the original goal as the main detail, but provide the plan from the
// first agent as context.
String writerTaskDetails = String.format(
"The user wants to achieve this goal: '%s'. Your task is to write the final content based on the following plan:\n%s",
highLevelGoal, plan);
String finalContent = writerAgent.executeTask(writerTaskDetails, instructions.writerInstructions());
LOG.info("Writer Agent Output (Final Content):");
return finalContent;
}
}
Creating the REST Endpoint
Finally, create a simple REST endpoint to trigger the entire workflow.
Create a new file src/main/java/org/acme/TaskResource.java
:
package org.acme;
import jakarta.inject.Inject;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/tasks")
public class TaskResource {
@Inject
AgentSpawner agentSpawner;
@POST
@Produces(MediaType.TEXT_PLAIN)
public String createTask(String goal) {
if (goal == null || goal.isBlank()) {
return "Please provide a goal in the request body.";
}
return agentSpawner.spawnAndExecute(goal);
}
}
Running and Testing the Application
Your project is complete! Let's run it and see it in action.
Run the Quarkus App: In your terminal, from the project root, run:
mvn quarkus:dev
The application will start up, and you'll see the Quarkus logs.
Test with cURL: Open a new terminal window and use curl
to send a POST
request to your endpoint.
Test Case 1: A Simple Recipe
curl -X POST -H "Content-Type: text/plain" \
--data "Write a short, upbeat, and encouraging twitter post about learning Java." \
http://localhost:8080/tasks
Terminal Output (Application Side): You will see the logs from the AgentSpawner
service, showing the intermediate step.
INFO Generating decomposed instructions for goal: Write a short, upbeat, and encouraging twitter post about learning Java.
INFO Generated Planner Instructions: Identify key points to convey excitement and motivation in learning Java on Twitter.
INFO Generated Writer Instructions: Transform the structured outline into a tweet-length message that includes relevant hashtags.
INFO Spawning PLANNER agent with model: dev.langchain4j.model.ollama.OllamaChatModel@2ed0d736
INFO Planner Agent Output (The Plan):
🚀 Ready for an adventure into coding? Start your journey with #Java! It's powerful & versatile – perfect to learn as you build apps that can change the world. Embrace every challenge, celebrate progress, and remember - practice makes perfect! Let'self start a Java revolution today 🌐✨#CodeNewbie
INFO Spawning WRITER agent with model: dev.langchain4j.model.ollama.OllamaChatModel@ddee987
INFO Writer Agent Output (Final Content): 🚀 Ready to embark on an coding adventure? 🚀 #Java awaits your curious mind! This versatile language is perfect for beginners, empowering you to build apps that can leave a lasting impact. Embrace the challenges, celebrate milestones, and remember, practice makes perfect. Let's join the Java revolution together! 🌐✨ #CodeNewbie
cURL Output (Client Side): You'll get the final result from the worker agent.
🚀 Ready to embark on an coding adventure? 🚀 #Java awaits your curious mind! This versatile language is perfect for beginners, empowering you to build apps that can leave a lasting impact. Embrace the challenges, celebrate milestones, and remember, practice makes perfect. Let's join the Java revolution together! 🌐✨ #CodeNewbie
Conclusion & Further Reading
Congratulations!You have successfully built an advanced AI application that demonstrates a powerful orchestration pattern. You learned how to:
Generate structured JSON output from an LLM.
Configure and use multiple language models in a single application.
Use the library's custom
@ModelName
annotation for type-safe dependency injection.Dynamically spawn multiple agents, each with a distinct model and instructions.
Chain agents together, using the output of one as the input for another.
While our tutorial implemented a sequential chain (Planner → Writer), which is a fundamental building block, the architecture of agent-based systems can evolve much further. For instance, you could implement a routing pattern, where a primary "router" agent analyzes incoming requests and intelligently dispatches the task to the most suitable specialist agent from a pool of experts. Much like a dispatcher sending a specific job to the right team. Another advanced technique is the parallelization pattern, where a complex problem is broken down and assigned to multiple agents that work concurrently. This "divide and conquer" approach can dramatically speed up tasks like researching several topics at once before synthesizing a final answer. Combining these patterns of chaining, routing, and parallelization allows you to construct truly dynamic and efficient autonomous systems.
The official Quarkus blog offers a great three-part series that provides a comprehensive overview of the concepts we've touched upon, guiding you from foundational principles to building sophisticated, tool-using AI agents with Langchain4j: Part 1: The Basics, Part 2: Let the Agent Use Tools, and Part 3: The Planner-Agent.
Next step? Add memory or retrieval tools. Or build a frontend to visualize agent output.
Let the agents do the heavy lifting. You just write Java.