Build AI agents with LangChain4j - basic agent, memory, tools/MCP, agentic workflows, guardrails, and observability
90
90%
Does it follow best practices?
Impact
90%
2.90xAverage score across 3 eval scenarios
Passed
No known issues
Step-by-step guide for building AI agents using LangChain4j 1.13+ and Quarkus. Covers basic agents, memory, tools, MCP, agentic workflows, guardrails, and observability.
<dependencyManagement>
<dependencies>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-bom</artifactId>
<version>1.13.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>| Artifact | Purpose |
|---|---|
dev.langchain4j:langchain4j | Core framework |
dev.langchain4j:langchain4j-open-ai | OpenAI models |
dev.langchain4j:langchain4j-anthropic | Anthropic Claude models |
dev.langchain4j:langchain4j-mcp-client | MCP client support |
dev.langchain4j:langchain4j-agentic | Multi-agent orchestration |
io.quarkiverse.langchain4j)| Artifact | Purpose |
|---|---|
quarkus-langchain4j-openai | OpenAI + Quarkus integration |
quarkus-langchain4j-anthropic | Anthropic + Quarkus integration |
quarkus-langchain4j-mcp | MCP + Quarkus integration |
Minimum JDK: 17
Checkpoint: Run
mvn compileto confirm dependencies resolve before proceeding.
import io.quarkiverse.langchain4j.RegisterAiService;
@RegisterAiService
public interface Agent {
@SystemMessage("You are a personal AI assistant")
String chat(String message);
}
// Inject and use:
@Inject Agent agent;
String response = agent.chat("Hello!");application.properties:
quarkus.langchain4j.anthropic.api-key=${ANTHROPIC_API_KEY}
quarkus.langchain4j.anthropic.chat-model.model-name=claude-sonnet-4-20250514import dev.langchain4j.service.AiServices;
public interface Agent {
@SystemMessage("You are a personal AI assistant")
String chat(String message);
}
Agent agent = AiServices.builder(Agent.class)
.chatModel(model)
.build();String -- raw LLM textboolean, int -- structured extractionResult<T> -- wraps any type with TokenUsage, sources, tool executionsTokenStream -- streaming token-by-tokenCheckpoint: Verify
agent.chat("Hello!")returns a non-null response before adding memory or tools.
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
// Quarkus:
@RegisterAiService(
chatMemoryProviderSupplier = MyMemoryProvider.class
)
public interface Agent {
String chat(@MemoryId String userId, String message);
}
// Plain Java — per-user memory:
Agent agent = AiServices.builder(Agent.class)
.chatModel(model)
.chatMemoryProvider(memoryId -> MessageWindowChatMemory.builder()
.id(memoryId)
.maxMessages(20)
.chatMemoryStore(store) // optional for persistence
.build())
.build();import dev.langchain4j.store.memory.chat.ChatMemoryStore;
public interface ChatMemoryStore {
List<ChatMessage> getMessages(Object memoryId);
void updateMessages(Object memoryId, List<ChatMessage> messages);
void deleteMessages(Object memoryId);
}import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.agent.tool.P;
@ApplicationScoped // CDI bean for Quarkus
public class CalendarTools {
@Tool("Get meetings for a specific date")
public String getMeetings(
@P("Date in YYYY-MM-DD format") String date
) {
return calendarService.getMeetings(date);
}
@Tool("Create a new calendar event")
public String createEvent(
@P("Event title") String title,
@P("Start time in ISO format") String startTime,
@P("Duration in minutes") int durationMinutes
) {
return calendarService.create(title, startTime, durationMinutes);
}
}// Quarkus:
@RegisterAiService(tools = CalendarTools.class)
public interface Agent { String chat(String message); }
// Plain Java:
Agent agent = AiServices.builder(Agent.class)
.chatModel(model)
.tools(new CalendarTools())
.build();import dev.langchain4j.service.tool.ToolProvider;
ToolProvider provider = (ToolProviderRequest request) ->
ToolProviderResult.builder()
.add(toolSpec, toolExecutor)
.build();
Agent agent = AiServices.builder(Agent.class)
.chatModel(model)
.toolProvider(provider)
.build();Checkpoint: Confirm tool methods are invoked by asking the agent a question that requires them (e.g., "What meetings do I have today?") and checking logs for tool execution before adding MCP.
import dev.langchain4j.mcp.client.DefaultMcpClient;
import dev.langchain4j.mcp.client.McpToolProvider;
import dev.langchain4j.mcp.client.transport.StdioMcpTransport;
// 1. Create transport (see options below)
McpTransport transport = StdioMcpTransport.builder()
.command(List.of("developer-events-mcp"))
.build();
// 2. Create MCP client
McpClient mcpClient = DefaultMcpClient.builder()
.transport(transport)
.build();
// 3. Create tool provider — optionally filter tool names
McpToolProvider toolProvider = McpToolProvider.builder()
.mcpClients(mcpClient)
.filterToolNames("search_cfps_by_keyword", "list_open_cfps") // optional
.build();
// 4. Use with AiServices
Agent agent = AiServices.builder(Agent.class)
.chatModel(model)
.toolProvider(toolProvider)
.build();// STDIO (local subprocess)
StdioMcpTransport.builder()
.command(List.of("npx", "@modelcontextprotocol/server-everything"))
.build();
// Streamable HTTP (remote, recommended)
StreamableHttpMcpTransport.builder()
.url("http://localhost:3001/mcp")
.build();
// Docker
DockerMcpTransport.builder()
.image("mcp/time")
.build();application.properties:
quarkus.langchain4j.mcp.events.transport-type=stdio
quarkus.langchain4j.mcp.events.command=developer-events-mcpimport io.quarkiverse.langchain4j.mcp.runtime.McpToolBox;
@RegisterAiService
public interface Agent {
@McpToolBox("events") // use tools from "events" MCP server
String chat(String message);
}Dependency: dev.langchain4j:langchain4j-agentic
import dev.langchain4j.agentic.AgenticServices;
import dev.langchain4j.agentic.Agent;
public interface EmailAgent {
@Agent(description = "Handles email-related requests", outputKey = "emailResult")
String handleEmail(@V("request") String request);
}
UntypedAgent emailAgent = AgenticServices.agentBuilder(EmailAgent.class)
.chatModel(model)
.tools(new EmailTools())
.outputKey("emailResult")
.build();// Sequential
UntypedAgent pipeline = AgenticServices.sequenceBuilder()
.subAgents(classifierAgent, handlerAgent, responseAgent)
.outputKey("finalResult")
.build();
// Parallel
UntypedAgent parallel = AgenticServices.parallelBuilder(PlannerAgent.class)
.subAgents(emailAgent, calendarAgent, weatherAgent)
.executor(Executors.newFixedThreadPool(3))
.output(scope -> scope.readState("emailResult", "") + "\n" + scope.readState("calResult", ""))
.outputKey("combinedResult")
.build();
// Conditional
UntypedAgent conditional = AgenticServices.conditionalBuilder()
.subAgents(scope -> "email".equals(scope.readState("category")), emailAgent)
.subAgents(scope -> "calendar".equals(scope.readState("category")), calendarAgent)
.build();
// Loop
UntypedAgent loop = AgenticServices.loopBuilder()
.subAgents(drafterAgent, reviewerAgent)
.maxIterations(3)
.exitCondition(scope -> scope.readState("approved", false))
.build();import dev.langchain4j.agentic.SupervisorAgent;
import dev.langchain4j.agentic.SupervisorResponseStrategy;
SupervisorAgent supervisor = AgenticServices.supervisorBuilder()
.chatModel(plannerModel)
.subAgents(emailAgent, calendarAgent, webSearchAgent)
.supervisorContext("Route user requests to the appropriate specialist agent")
.responseStrategy(SupervisorResponseStrategy.SUMMARY)
.build();
String result = supervisor.invoke("Schedule a meeting with John next Tuesday");Response strategies: LAST (default), SUMMARY, SCORED
scope.writeState("category", "email");
String cat = scope.readState("category", "unknown");
// Type-safe keys
public static class EmailResult implements TypedKey<String> {}
scope.writeState(new EmailResult(), "Done");@SequentialAgent
public interface Pipeline {
@AgentMember ClassifierAgent classifier();
@AgentMember EmailAgent emailHandler();
@AgentMember CalendarAgent calendarHandler();
}Checkpoint: Invoke each sub-agent independently with
.invoke()and confirm correct output before composing into a pipeline or supervisor.
Status: EXPERIMENTAL
import dev.langchain4j.guardrail.InputGuardrail;
import dev.langchain4j.guardrail.InputGuardrailResult;
public class PromptInjectionGuard implements InputGuardrail {
@Override
public InputGuardrailResult validate(UserMessage userMessage) {
if (userMessage.singleText().toLowerCase().contains("ignore all previous")) {
return fatal("Prompt injection detected");
}
return success();
}
}Result methods: success(), successWith(alteredMessage), failure(reason), fatal(reason)
import dev.langchain4j.guardrail.OutputGuardrail;
import dev.langchain4j.guardrail.OutputGuardrailResult;
public class ToxicityGuard implements OutputGuardrail {
@Override
public OutputGuardrailResult validate(AiMessage response) {
if (isToxic(response.text())) {
return reprompt("Response was toxic", "Please rephrase politely");
}
return success();
}
}Result methods: success(), successWith(rewritten), failure(reason), fatal(reason), retry(reason), reprompt(reason, newPrompt)
import dev.langchain4j.service.guardrail.InputGuardrails;
import dev.langchain4j.service.guardrail.OutputGuardrails;
@InputGuardrails(PromptInjectionGuard.class)
@OutputGuardrails(value = ToxicityGuard.class, maxRetries = 3)
public interface Agent {
String chat(String message);
}import dev.langchain4j.agentic.AgentListener;
public class LoggingListener implements AgentListener {
@Override
public void beforeAgentInvocation(AgentRequest request) {
log.info("Agent starting: {}", request.agentName());
}
@Override
public void afterAgentInvocation(AgentResponse response) {
log.info("Agent done: {} ({}ms, {} tokens)",
response.agentName(),
response.durationMs(),
response.tokenUsage().totalTokenCount());
}
@Override
public void beforeToolExecution(BeforeToolExecution exec) {
log.info("Calling tool: {}", exec.toolName());
}
@Override
public boolean inheritedBySubagents() { return true; }
}import dev.langchain4j.agentic.AgentMonitor;
AgentMonitor monitor = AgentMonitor.create();
UntypedAgent agent = AgenticServices.agentBuilder(MyAgent.class)
.chatModel(model)
.listener(monitor)
.build();
agent.invoke("Do something");
String html = monitor.generateReport();
Files.writeString(Path.of("agent-report.html"), html);This end-to-end example wires together memory, tools, MCP, and guardrails.
pom.xml (key dependencies, use BOM):
<dependency>
<groupId>io.quarkiverse.langchain4j</groupId>
<artifactId>quarkus-langchain4j-anthropic</artifactId>
</dependency>
<dependency>
<groupId>io.quarkiverse.langchain4j</groupId>
<artifactId>quarkus-langchain4j-mcp</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-agentic</artifactId>
</dependency>Agent + tools + guardrail:
@RegisterAiService(tools = LocalTools.class)
@InputGuardrails(PromptInjectionGuard.class)
public interface PersonalAgent {
@SystemMessage("You are a personal AI assistant. Help with calendar, email, and tasks.")
@McpToolBox("events")
String chat(@MemoryId String userId, @UserMessage String message);
}
@ApplicationScoped
public class LocalTools {
@Tool("Get the current date and time")
public String getCurrentTime() { return LocalDateTime.now().toString(); }
}
@ApplicationScoped
public class PromptInjectionGuard implements InputGuardrail {
@Override
public InputGuardrailResult validate(UserMessage msg) {
if (msg.singleText().toLowerCase().contains("ignore all previous"))
return fatal("Blocked");
return success();
}
}application.properties:
quarkus.langchain4j.anthropic.api-key=${ANTHROPIC_API_KEY}
quarkus.langchain4j.anthropic.chat-model.model-name=claude-sonnet-4-20250514
quarkus.langchain4j.mcp.events.transport-type=stdio
quarkus.langchain4j.mcp.events.command=developer-events-mcpREST endpoint:
@Path("/chat")
@ApplicationScoped
public class ChatResource {
@Inject PersonalAgent agent;
@POST
public String chat(@QueryParam("userId") String userId, String message) {
return agent.chat(userId, message);
}
}